This commit is contained in:
Michael Freno
2025-10-13 17:26:20 -04:00
parent 560d4fd32c
commit 7496367f85
4 changed files with 576 additions and 270 deletions

View File

@@ -372,15 +372,16 @@ end
local NineSlice = {}
--- Draw a 9-slice component
--- Draw a 9-slice component with borders in padding area
---@param component ThemeComponent
---@param atlas love.Image
---@param x number
---@param y number
---@param width number
---@param height number
---@param x number -- X position of border box (top-left corner)
---@param y number -- Y position of border box (top-left corner)
---@param contentWidth number -- Width of content area (excludes padding)
---@param contentHeight number -- Height of content area (excludes padding)
---@param padding {top:number, right:number, bottom:number, left:number} -- Padding defines border thickness
---@param opacity number?
function NineSlice.draw(component, atlas, x, y, width, height, opacity)
function NineSlice.draw(component, atlas, x, y, contentWidth, contentHeight, padding, opacity)
if not component or not atlas then
return
end
@@ -390,19 +391,20 @@ function NineSlice.draw(component, atlas, x, y, width, height, opacity)
local regions = component.regions
-- Calculate dimensions
local cornerWidth = regions.topLeft.w
local cornerHeight = regions.topLeft.h
local rightCornerWidth = regions.topRight.w
local rightCornerHeight = regions.topRight.h
local bottomLeftHeight = regions.bottomLeft.h
local bottomRightHeight = regions.bottomRight.h
local bottomLeftWidth = regions.bottomLeft.w
local bottomRightWidth = regions.bottomRight.w
-- Calculate source image border dimensions from regions
local sourceBorderLeft = regions.topLeft.w
local sourceBorderRight = regions.topRight.w
local sourceBorderTop = regions.topLeft.h
local sourceBorderBottom = regions.bottomLeft.h
local sourceCenterWidth = regions.middleCenter.w
local sourceCenterHeight = regions.middleCenter.h
-- Calculate minimum required dimensions
local minWidth = cornerWidth + rightCornerWidth
local minHeight = cornerHeight + bottomLeftHeight
-- Calculate scale factors to fit borders within padding
-- Borders scale to fit the padding dimensions
local scaleLeft = padding.left / sourceBorderLeft
local scaleRight = padding.right / sourceBorderRight
local scaleTop = padding.top / sourceBorderTop
local scaleBottom = padding.bottom / sourceBorderBottom
-- Create quads for each region
local atlasWidth, atlasHeight = atlas:getDimensions()
@@ -412,117 +414,78 @@ function NineSlice.draw(component, atlas, x, y, width, height, opacity)
return love.graphics.newQuad(region.x, region.y, region.w, region.h, atlasWidth, atlasHeight)
end
-- Check if element is too small and needs proportional scaling
local scaleDownX = 1
local scaleDownY = 1
-- Top-left corner (scales to fit top-left padding)
love.graphics.draw(atlas, makeQuad(regions.topLeft), x, y, 0, scaleLeft, scaleTop)
if width < minWidth then
scaleDownX = width / minWidth
end
-- Top-right corner (scales to fit top-right padding)
love.graphics.draw(atlas, makeQuad(regions.topRight), x + padding.left + contentWidth, y, 0, scaleRight, scaleTop)
if height < minHeight then
scaleDownY = height / minHeight
end
-- Bottom-left corner (scales to fit bottom-left padding)
love.graphics.draw(atlas, makeQuad(regions.bottomLeft), x, y + padding.top + contentHeight, 0, scaleLeft, scaleBottom)
-- Apply proportional scaling to corner dimensions if needed
local scaledCornerWidth = cornerWidth * scaleDownX
local scaledRightCornerWidth = rightCornerWidth * scaleDownX
local scaledCornerHeight = cornerHeight * scaleDownY
local scaledBottomLeftHeight = bottomLeftHeight * scaleDownY
local scaledBottomRightHeight = bottomRightHeight * scaleDownY
-- Center dimensions (stretchable area)
local centerWidth = width - scaledCornerWidth - scaledRightCornerWidth
local centerHeight = height - scaledCornerHeight - scaledBottomLeftHeight
-- Top-left corner
love.graphics.draw(atlas, makeQuad(regions.topLeft), x, y, 0, scaleDownX, scaleDownY)
-- Top-right corner
love.graphics.draw(
atlas,
makeQuad(regions.topRight),
x + width - scaledRightCornerWidth,
y,
0,
scaleDownX,
scaleDownY
)
-- Bottom-left corner
love.graphics.draw(
atlas,
makeQuad(regions.bottomLeft),
x,
y + height - scaledBottomLeftHeight,
0,
scaleDownX,
scaleDownY
)
-- Bottom-right corner
-- Bottom-right corner (scales to fit bottom-right padding)
love.graphics.draw(
atlas,
makeQuad(regions.bottomRight),
x + width - scaledRightCornerWidth,
y + height - scaledBottomRightHeight,
x + padding.left + contentWidth,
y + padding.top + contentHeight,
0,
scaleDownX,
scaleDownY
scaleRight,
scaleBottom
)
-- Top edge (stretched)
if centerWidth > 0 then
local scaleX = centerWidth / regions.topCenter.w
love.graphics.draw(atlas, makeQuad(regions.topCenter), x + scaledCornerWidth, y, 0, scaleX, scaleDownY)
-- Top edge (stretched to content width, scaled to padding.top height)
if contentWidth > 0 then
local stretchScaleX = contentWidth / sourceCenterWidth
love.graphics.draw(atlas, makeQuad(regions.topCenter), x + padding.left, y, 0, stretchScaleX, scaleTop)
end
-- Bottom edge (stretched)
if centerWidth > 0 then
local scaleX = centerWidth / regions.bottomCenter.w
-- Bottom edge (stretched to content width, scaled to padding.bottom height)
if contentWidth > 0 then
local stretchScaleX = contentWidth / sourceCenterWidth
love.graphics.draw(
atlas,
makeQuad(regions.bottomCenter),
x + scaledCornerWidth,
y + height - scaledBottomLeftHeight,
x + padding.left,
y + padding.top + contentHeight,
0,
scaleX,
scaleDownY
stretchScaleX,
scaleBottom
)
end
-- Left edge (stretched)
if centerHeight > 0 then
local scaleY = centerHeight / regions.middleLeft.h
love.graphics.draw(atlas, makeQuad(regions.middleLeft), x, y + scaledCornerHeight, 0, scaleDownX, scaleY)
-- Left edge (scaled to padding.left width, stretched to content height)
if contentHeight > 0 then
local stretchScaleY = contentHeight / sourceCenterHeight
love.graphics.draw(atlas, makeQuad(regions.middleLeft), x, y + padding.top, 0, scaleLeft, stretchScaleY)
end
-- Right edge (stretched)
if centerHeight > 0 then
local scaleY = centerHeight / regions.middleRight.h
-- Right edge (scaled to padding.right width, stretched to content height)
if contentHeight > 0 then
local stretchScaleY = contentHeight / sourceCenterHeight
love.graphics.draw(
atlas,
makeQuad(regions.middleRight),
x + width - scaledRightCornerWidth,
y + scaledCornerHeight,
x + padding.left + contentWidth,
y + padding.top,
0,
scaleDownX,
scaleY
scaleRight,
stretchScaleY
)
end
-- Center (stretched both ways)
if centerWidth > 0 and centerHeight > 0 then
local scaleX = centerWidth / regions.middleCenter.w
local scaleY = centerHeight / regions.middleCenter.h
-- Center (stretched to fill content area)
if contentWidth > 0 and contentHeight > 0 then
local stretchScaleX = contentWidth / sourceCenterWidth
local stretchScaleY = contentHeight / sourceCenterHeight
love.graphics.draw(
atlas,
makeQuad(regions.middleCenter),
x + scaledCornerWidth,
y + scaledCornerHeight,
x + padding.left,
y + padding.top,
0,
scaleX,
scaleY
stretchScaleX,
stretchScaleY
)
end
@@ -600,6 +563,7 @@ local enums = {
-- Text size preset mappings (in vh units for auto-scaling)
local TEXT_SIZE_PRESETS = {
["2xs"] = 0.75, -- 0.75vh
xxs = 0.75, -- 0.75vh
xs = 1.25, -- 1.25vh
sm = 1.75, -- 1.75vh
@@ -607,6 +571,7 @@ local TEXT_SIZE_PRESETS = {
lg = 2.75, -- 2.75vh
xl = 3.5, -- 3.5vh
xxl = 4.5, -- 4.5vh
["2xl"] = 4.5, -- 4.5vh
["3xl"] = 5.0, -- 5vh
["4xl"] = 7.0, -- 7vh
}
@@ -794,28 +759,29 @@ function Grid.layoutGridItems(element)
for _, child in ipairs(element.children) do
-- Only consider absolutely positioned children with explicit positioning
if child.positioning == Positioning.ABSOLUTE and child._explicitlyAbsolute then
-- BORDER-BOX MODEL: Use border-box dimensions for space calculations
local childBorderBoxWidth = child:getBorderBoxWidth()
local childBorderBoxHeight = child:getBorderBoxHeight()
if child.left then
local childTotalWidth = (child.width or 0) + child.padding.left + child.padding.right
reservedLeft = math.max(reservedLeft, child.left + childTotalWidth)
reservedLeft = math.max(reservedLeft, child.left + childBorderBoxWidth)
end
if child.right then
local childTotalWidth = (child.width or 0) + child.padding.left + child.padding.right
reservedRight = math.max(reservedRight, child.right + childTotalWidth)
reservedRight = math.max(reservedRight, child.right + childBorderBoxWidth)
end
if child.top then
local childTotalHeight = (child.height or 0) + child.padding.top + child.padding.bottom
reservedTop = math.max(reservedTop, child.top + childTotalHeight)
reservedTop = math.max(reservedTop, child.top + childBorderBoxHeight)
end
if child.bottom then
local childTotalHeight = (child.height or 0) + child.padding.top + child.padding.bottom
reservedBottom = math.max(reservedBottom, child.bottom + childTotalHeight)
reservedBottom = math.max(reservedBottom, child.bottom + childBorderBoxHeight)
end
end
end
-- Calculate available space (accounting for padding and reserved space)
local availableWidth = element.width - element.padding.left - element.padding.right - reservedLeft - reservedRight
local availableHeight = element.height - element.padding.top - element.padding.bottom - reservedTop - reservedBottom
-- BORDER-BOX MODEL: element.width and element.height are already content dimensions
local availableWidth = element.width - reservedLeft - reservedRight
local availableHeight = element.height - reservedTop - reservedBottom
-- Get gaps
local columnGap = element.columnGap or 0
@@ -855,19 +821,22 @@ function Grid.layoutGridItems(element)
local effectiveAlignItems = element.alignItems or AlignItems.STRETCH
-- Stretch child to fill cell by default
-- BORDER-BOX MODEL: Set border-box dimensions, content area adjusts automatically
if effectiveAlignItems == AlignItems.STRETCH or effectiveAlignItems == "stretch" then
child.x = cellX
child.y = cellY
child.width = cellWidth - child.padding.left - child.padding.right
child.height = cellHeight - child.padding.top - child.padding.bottom
child._borderBoxWidth = cellWidth
child._borderBoxHeight = cellHeight
child.width = math.max(0, cellWidth - child.padding.left - child.padding.right)
child.height = math.max(0, cellHeight - child.padding.top - child.padding.bottom)
-- Disable auto-sizing when stretched by grid
child.autosizing.width = false
child.autosizing.height = false
elseif effectiveAlignItems == AlignItems.CENTER or effectiveAlignItems == "center" then
local childTotalWidth = child.width + child.padding.left + child.padding.right
local childTotalHeight = child.height + child.padding.top + child.padding.bottom
child.x = cellX + (cellWidth - childTotalWidth) / 2
child.y = cellY + (cellHeight - childTotalHeight) / 2
local childBorderBoxWidth = child:getBorderBoxWidth()
local childBorderBoxHeight = child:getBorderBoxHeight()
child.x = cellX + (cellWidth - childBorderBoxWidth) / 2
child.y = cellY + (cellHeight - childBorderBoxHeight) / 2
elseif
effectiveAlignItems == AlignItems.FLEX_START
or effectiveAlignItems == "flex-start"
@@ -880,16 +849,18 @@ function Grid.layoutGridItems(element)
or effectiveAlignItems == "flex-end"
or effectiveAlignItems == "end"
then
local childTotalWidth = child.width + child.padding.left + child.padding.right
local childTotalHeight = child.height + child.padding.top + child.padding.bottom
child.x = cellX + cellWidth - childTotalWidth
child.y = cellY + cellHeight - childTotalHeight
local childBorderBoxWidth = child:getBorderBoxWidth()
local childBorderBoxHeight = child:getBorderBoxHeight()
child.x = cellX + cellWidth - childBorderBoxWidth
child.y = cellY + cellHeight - childBorderBoxHeight
else
-- Default to stretch
child.x = cellX
child.y = cellY
child.width = cellWidth - child.padding.left - child.padding.right
child.height = cellHeight - child.padding.top - child.padding.bottom
child._borderBoxWidth = cellWidth
child._borderBoxHeight = cellHeight
child.width = math.max(0, cellWidth - child.padding.left - child.padding.right)
child.height = math.max(0, cellHeight - child.padding.top - child.padding.bottom)
-- Disable auto-sizing when stretched by grid
child.autosizing.width = false
child.autosizing.height = false
@@ -1274,7 +1245,7 @@ end
---@field gap number|string -- Space between children elements (default: 10)
---@field padding {top?:number, right?:number, bottom?:number, left?:number}? -- Padding around children (default: {top=0, right=0, bottom=0, left=0})
---@field margin {top?:number, right?:number, bottom?:number, left?:number} -- Margin around children (default: {top=0, right=0, bottom=0, left=0})
---@field positioning Positioning -- Layout positioning mode (default: ABSOLUTE)
---@field positioning Positioning -- Layout positioning mode (default: RELATIVE)
---@field flexDirection FlexDirection -- Direction of flex layout (default: HORIZONTAL)
---@field justifyContent JustifyContent -- Alignment of items along main axis (default: FLEX_START)
---@field alignItems AlignItems -- Alignment of items along cross axis (default: STRETCH)
@@ -1333,7 +1304,7 @@ Element.__index = Element
---@field textSize number|string? -- Font size: number (px), string with units ("2vh", "10%"), or preset ("xxs"|"xs"|"sm"|"md"|"lg"|"xl"|"xxl"|"3xl"|"4xl") (default: "md")
---@field fontFamily string? -- Font family name from theme or path to font file (default: theme default or system default)
---@field autoScaleText boolean? -- Whether text should auto-scale with window size (default: true)
---@field positioning Positioning? -- Layout positioning mode (default: ABSOLUTE)
---@field positioning Positioning? -- Layout positioning mode (default: RELATIVE)
---@field flexDirection FlexDirection? -- Direction of flex layout (default: HORIZONTAL)
---@field justifyContent JustifyContent? -- Alignment of items along main axis (default: FLEX_START)
---@field alignItems AlignItems? -- Alignment of items along cross axis (default: STRETCH)
@@ -1494,8 +1465,11 @@ function Element.new(props)
if props.themeComponent and not props.fontFamily then
local themeToUse = self.theme and themes[self.theme] or Theme.getActive()
if themeToUse and themeToUse.fonts then
-- Use default font from theme if available
self.fontFamily = "default"
if self.parent then
self.fontFamily = self.parent.fontFamily
else
self.fontFamily = "default"
end
end
end
@@ -1579,39 +1553,47 @@ function Element.new(props)
-- Handle width (both w and width properties, prefer w if both exist)
local widthProp = props.width
local tempWidth = 0 -- Temporary width for padding resolution
if widthProp then
if type(widthProp) == "string" then
local value, unit = Units.parse(widthProp)
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)
tempWidth = Units.resolve(value, unit, viewportWidth, viewportHeight, parentWidth)
else
-- Apply base scaling to pixel values
self.width = Gui.baseScale and (widthProp * scaleX) or widthProp
tempWidth = Gui.baseScale and (widthProp * scaleX) or widthProp
self.units.width = { value = widthProp, unit = "px" }
end
self.width = tempWidth
else
self.autosizing.width = true
self.width = self:calculateAutoWidth()
-- Calculate auto-width without padding first
tempWidth = self:calculateAutoWidth()
self.width = tempWidth
self.units.width = { value = nil, unit = "auto" } -- Mark as auto-sized
end
-- Handle height (both h and height properties, prefer h if both exist)
local heightProp = props.height
local tempHeight = 0 -- Temporary height for padding resolution
if heightProp then
if type(heightProp) == "string" then
local value, unit = Units.parse(heightProp)
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)
tempHeight = Units.resolve(value, unit, viewportWidth, viewportHeight, parentHeight)
else
-- Apply base scaling to pixel values
self.height = Gui.baseScale and (heightProp * scaleY) or heightProp
tempHeight = Gui.baseScale and (heightProp * scaleY) or heightProp
self.units.height = { value = heightProp, unit = "px" }
end
self.height = tempHeight
else
self.autosizing.height = true
self.height = self:calculateAutoHeight()
-- Calculate auto-height without padding first
tempHeight = self:calculateAutoHeight()
self.height = tempHeight
self.units.height = { value = nil, unit = "auto" } -- Mark as auto-sized
end
@@ -1630,14 +1612,40 @@ function Element.new(props)
self.units.gap = { value = props.gap, unit = "px" }
end
else
self.gap = 10
self.units.gap = { value = 10, unit = "px" }
self.gap = 0
self.units.gap = { value = 0, 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)
-- BORDER-BOX MODEL: For auto-sizing, we need to add padding to content dimensions
-- For explicit sizing, width/height already include padding (border-box)
-- 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)
-- For auto-sized elements, add padding to get border-box dimensions
if self.autosizing.width then
self._borderBoxWidth = self.width + tempPadding.left + tempPadding.right
else
-- For explicit sizing, width is already border-box
self._borderBoxWidth = self.width
end
if self.autosizing.height then
self._borderBoxHeight = self.height + tempPadding.top + tempPadding.bottom
else
-- For explicit sizing, height is already border-box
self._borderBoxHeight = self.height
end
-- Re-resolve padding based on final border-box dimensions (important for percentage padding)
self.padding = Units.resolveSpacing(props.padding, self._borderBoxWidth, self._borderBoxHeight)
-- Calculate final content dimensions by subtracting padding from border-box
self.width = math.max(0, self._borderBoxWidth - self.padding.left - self.padding.right)
self.height = math.max(0, self._borderBoxHeight - self.padding.top - self.padding.bottom)
-- Re-resolve ew/eh textSize units now that width/height are set
if props.textSize and type(props.textSize) == "string" then
local value, unit = Units.parse(props.textSize)
@@ -1745,7 +1753,7 @@ function Element.new(props)
self._originalPositioning = props.positioning
self._explicitlyAbsolute = (props.positioning == Positioning.ABSOLUTE)
else
self.positioning = Positioning.ABSOLUTE
self.positioning = Positioning.RELATIVE
self._originalPositioning = nil -- No explicit positioning
self._explicitlyAbsolute = false
end
@@ -1763,13 +1771,13 @@ function Element.new(props)
self._explicitlyAbsolute = false
else
-- Default: children in flex/grid containers participate in parent's layout
-- children in absolute containers default to absolute
-- children in relative/absolute containers default to relative
if self.parent.positioning == Positioning.FLEX or self.parent.positioning == Positioning.GRID then
self.positioning = Positioning.ABSOLUTE -- They are positioned BY flex/grid, not AS flex/grid
self._explicitlyAbsolute = false -- Participate in parent's layout
else
self.positioning = Positioning.ABSOLUTE
self._explicitlyAbsolute = false -- Default for absolute containers
self.positioning = Positioning.RELATIVE
self._explicitlyAbsolute = false -- Default for relative/absolute containers
end
end
@@ -1968,12 +1976,24 @@ function Element.new(props)
return self
end
--- Get element bounds
--- Get element bounds (content box)
---@return { x:number, y:number, width:number, height:number }
function Element:getBounds()
return { x = self.x, y = self.y, width = self.width, height = self.height }
end
--- Get border-box width (including padding)
---@return number
function Element:getBorderBoxWidth()
return self._borderBoxWidth or (self.width + self.padding.left + self.padding.right)
end
--- Get border-box height (including padding)
---@return number
function Element:getBorderBoxHeight()
return self._borderBoxHeight or (self.height + self.padding.top + self.padding.bottom)
end
--- Add child to element
---@param child Element
function Element:addChild(child)
@@ -1987,8 +2007,8 @@ function Element:addChild(child)
child.positioning = Positioning.ABSOLUTE -- They are positioned BY flex/grid, not AS flex/grid
child._explicitlyAbsolute = false -- Participate in parent's layout
else
child.positioning = Positioning.ABSOLUTE
child._explicitlyAbsolute = false -- Default for absolute containers
child.positioning = Positioning.RELATIVE
child._explicitlyAbsolute = false -- Default for relative/absolute containers
end
end
-- If child._originalPositioning is set, it means explicit positioning was provided
@@ -2000,10 +2020,16 @@ function Element:addChild(child)
-- (CSS: absolutely positioned children don't affect parent auto-sizing)
if not child._explicitlyAbsolute then
if self.autosizing.height then
self.height = self:calculateAutoHeight()
local contentHeight = self:calculateAutoHeight()
-- BORDER-BOX MODEL: Add padding to get border-box, then subtract to get content
self._borderBoxHeight = contentHeight + self.padding.top + self.padding.bottom
self.height = contentHeight
end
if self.autosizing.width then
self.width = self:calculateAutoWidth()
local contentWidth = self:calculateAutoWidth()
-- BORDER-BOX MODEL: Add padding to get border-box, then subtract to get content
self._borderBoxWidth = contentWidth + self.padding.left + self.padding.right
self.width = contentWidth
end
end
@@ -2029,10 +2055,10 @@ function Element:applyPositioningOffsets(element)
end
-- Apply bottom offset (distance from parent's content box bottom edge)
-- Element's total height includes its padding
-- BORDER-BOX MODEL: Use border-box dimensions for positioning
if element.bottom then
local elementTotalHeight = element.height + element.padding.top + element.padding.bottom
element.y = parent.y + parent.height + parent.padding.top - element.bottom - elementTotalHeight
local elementBorderBoxHeight = element:getBorderBoxHeight()
element.y = parent.y + parent.padding.top + parent.height - element.bottom - elementBorderBoxHeight
end
-- Apply left offset (distance from parent's content box left edge)
@@ -2041,16 +2067,16 @@ function Element:applyPositioningOffsets(element)
end
-- Apply right offset (distance from parent's content box right edge)
-- Element's total width includes its padding
-- BORDER-BOX MODEL: Use border-box dimensions for positioning
if element.right then
local elementTotalWidth = element.width + element.padding.left + element.padding.right
element.x = parent.x + parent.width + parent.padding.left - element.right - elementTotalWidth
local elementBorderBoxWidth = element:getBorderBoxWidth()
element.x = parent.x + parent.padding.left + parent.width - element.right - elementBorderBoxWidth
end
end
function Element:layoutChildren()
if self.positioning == Positioning.ABSOLUTE then
-- Absolute positioned containers don't layout their children according to flex rules,
if self.positioning == Positioning.ABSOLUTE or self.positioning == Positioning.RELATIVE then
-- Absolute/Relative positioned containers don't layout their children according to flex rules,
-- but they should still apply CSS positioning offsets to their children
for _, child in ipairs(self.children) do
if child.top or child.right or child.bottom or child.left then
@@ -2094,56 +2120,52 @@ function Element:layoutChildren()
for _, child in ipairs(self.children) do
-- Only consider absolutely positioned children with explicit positioning
if child.positioning == Positioning.ABSOLUTE and child._explicitlyAbsolute then
-- BORDER-BOX MODEL: Use border-box dimensions for space calculations
local childBorderBoxWidth = child:getBorderBoxWidth()
local childBorderBoxHeight = child:getBorderBoxHeight()
if self.flexDirection == FlexDirection.HORIZONTAL then
-- Horizontal layout: main axis is X, cross axis is Y
-- Check for left positioning (reserves space at main axis start)
if child.left then
local childTotalWidth = (child.width or 0) + child.padding.left + child.padding.right
local spaceNeeded = child.left + childTotalWidth
local spaceNeeded = child.left + childBorderBoxWidth
reservedMainStart = math.max(reservedMainStart, spaceNeeded)
end
-- Check for right positioning (reserves space at main axis end)
if child.right then
local childTotalWidth = (child.width or 0) + child.padding.left + child.padding.right
local spaceNeeded = child.right + childTotalWidth
local spaceNeeded = child.right + childBorderBoxWidth
reservedMainEnd = math.max(reservedMainEnd, spaceNeeded)
end
-- Check for top positioning (reserves space at cross axis start)
if child.top then
local childTotalHeight = (child.height or 0) + child.padding.top + child.padding.bottom
local spaceNeeded = child.top + childTotalHeight
local spaceNeeded = child.top + childBorderBoxHeight
reservedCrossStart = math.max(reservedCrossStart, spaceNeeded)
end
-- Check for bottom positioning (reserves space at cross axis end)
if child.bottom then
local childTotalHeight = (child.height or 0) + child.padding.top + child.padding.bottom
local spaceNeeded = child.bottom + childTotalHeight
local spaceNeeded = child.bottom + childBorderBoxHeight
reservedCrossEnd = math.max(reservedCrossEnd, spaceNeeded)
end
else
-- Vertical layout: main axis is Y, cross axis is X
-- Check for top positioning (reserves space at main axis start)
if child.top then
local childTotalHeight = (child.height or 0) + child.padding.top + child.padding.bottom
local spaceNeeded = child.top + childTotalHeight
local spaceNeeded = child.top + childBorderBoxHeight
reservedMainStart = math.max(reservedMainStart, spaceNeeded)
end
-- Check for bottom positioning (reserves space at main axis end)
if child.bottom then
local childTotalHeight = (child.height or 0) + child.padding.top + child.padding.bottom
local spaceNeeded = child.bottom + childTotalHeight
local spaceNeeded = child.bottom + childBorderBoxHeight
reservedMainEnd = math.max(reservedMainEnd, spaceNeeded)
end
-- Check for left positioning (reserves space at cross axis start)
if child.left then
local childTotalWidth = (child.width or 0) + child.padding.left + child.padding.right
local spaceNeeded = child.left + childTotalWidth
local spaceNeeded = child.left + childBorderBoxWidth
reservedCrossStart = math.max(reservedCrossStart, spaceNeeded)
end
-- Check for right positioning (reserves space at cross axis end)
if child.right then
local childTotalWidth = (child.width or 0) + child.padding.left + child.padding.right
local spaceNeeded = child.right + childTotalWidth
local spaceNeeded = child.right + childBorderBoxWidth
reservedCrossEnd = math.max(reservedCrossEnd, spaceNeeded)
end
end
@@ -2151,14 +2173,15 @@ function Element:layoutChildren()
end
-- Calculate available space (accounting for padding and reserved space)
-- BORDER-BOX MODEL: self.width and self.height are already content dimensions (padding subtracted)
local availableMainSize = 0
local availableCrossSize = 0
if self.flexDirection == FlexDirection.HORIZONTAL then
availableMainSize = self.width - self.padding.left - self.padding.right - reservedMainStart - reservedMainEnd
availableCrossSize = self.height - self.padding.top - self.padding.bottom - reservedCrossStart - reservedCrossEnd
availableMainSize = self.width - reservedMainStart - reservedMainEnd
availableCrossSize = self.height - reservedCrossStart - reservedCrossEnd
else
availableMainSize = self.height - self.padding.top - self.padding.bottom - reservedMainStart - reservedMainEnd
availableCrossSize = self.width - self.padding.left - self.padding.right - reservedCrossStart - reservedCrossEnd
availableMainSize = self.height - reservedMainStart - reservedMainEnd
availableCrossSize = self.width - reservedCrossStart - reservedCrossEnd
end
-- Handle flex wrap: create lines of children
@@ -2173,11 +2196,12 @@ function Element:layoutChildren()
local currentLineSize = 0
for _, child in ipairs(flexChildren) do
-- BORDER-BOX MODEL: Use border-box dimensions for layout calculations
local childMainSize = 0
if self.flexDirection == FlexDirection.HORIZONTAL then
childMainSize = (child.width or 0) + child.padding.left + child.padding.right
childMainSize = child:getBorderBoxWidth()
else
childMainSize = (child.height or 0) + child.padding.top + child.padding.bottom
childMainSize = child:getBorderBoxHeight()
end
-- Check if adding this child would exceed the available space
@@ -2218,11 +2242,12 @@ function Element:layoutChildren()
for lineIndex, line in ipairs(lines) do
local maxCrossSize = 0
for _, child in ipairs(line) do
-- BORDER-BOX MODEL: Use border-box dimensions for layout calculations
local childCrossSize = 0
if self.flexDirection == FlexDirection.HORIZONTAL then
childCrossSize = (child.height or 0) + child.padding.top + child.padding.bottom
childCrossSize = child:getBorderBoxHeight()
else
childCrossSize = (child.width or 0) + child.padding.left + child.padding.right
childCrossSize = child:getBorderBoxWidth()
end
maxCrossSize = math.max(maxCrossSize, childCrossSize)
end
@@ -2289,14 +2314,13 @@ function Element:layoutChildren()
local lineHeight = lineHeights[lineIndex]
-- Calculate total size of children in this line (including padding)
-- BORDER-BOX MODEL: Use border-box dimensions for layout calculations
local totalChildrenSize = 0
for _, child in ipairs(line) do
if self.flexDirection == FlexDirection.HORIZONTAL then
local childTotalWidth = (child.width or 0) + child.padding.left + child.padding.right
totalChildrenSize = totalChildrenSize + childTotalWidth
totalChildrenSize = totalChildrenSize + child:getBorderBoxWidth()
else
local childTotalHeight = (child.height or 0) + child.padding.top + child.padding.bottom
totalChildrenSize = totalChildrenSize + childTotalHeight
totalChildrenSize = totalChildrenSize + child:getBorderBoxHeight()
end
end
@@ -2345,17 +2369,23 @@ function Element:layoutChildren()
-- Add reservedMainStart to account for absolutely positioned siblings
child.x = self.x + self.padding.left + reservedMainStart + currentMainPos
-- BORDER-BOX MODEL: Use border-box dimensions for alignment calculations
local childBorderBoxHeight = child:getBorderBoxHeight()
if effectiveAlign == AlignItems.FLEX_START then
child.y = self.y + self.padding.top + reservedCrossStart + currentCrossPos
elseif effectiveAlign == AlignItems.CENTER then
local childTotalHeight = (child.height or 0) + child.padding.top + child.padding.bottom
child.y = self.y + self.padding.top + reservedCrossStart + currentCrossPos + ((lineHeight - childTotalHeight) / 2)
child.y = self.y
+ self.padding.top
+ reservedCrossStart
+ currentCrossPos
+ ((lineHeight - childBorderBoxHeight) / 2)
elseif effectiveAlign == AlignItems.FLEX_END then
local childTotalHeight = (child.height or 0) + child.padding.top + child.padding.bottom
child.y = self.y + self.padding.top + reservedCrossStart + currentCrossPos + lineHeight - childTotalHeight
child.y = self.y + self.padding.top + reservedCrossStart + currentCrossPos + lineHeight - childBorderBoxHeight
elseif effectiveAlign == AlignItems.STRETCH then
-- STRETCH always stretches children in cross-axis direction
child.height = lineHeight - child.padding.top - child.padding.bottom
-- STRETCH: Set border-box height to lineHeight, content area shrinks to fit
child._borderBoxHeight = lineHeight
child.height = math.max(0, lineHeight - child.padding.top - child.padding.bottom)
child.y = self.y + self.padding.top + reservedCrossStart + currentCrossPos
end
@@ -2372,26 +2402,31 @@ function Element:layoutChildren()
child:layoutChildren()
end
-- Advance position by child's total width (width + padding)
local childTotalWidth = (child.width or 0) + child.padding.left + child.padding.right
currentMainPos = currentMainPos + childTotalWidth + itemSpacing
-- Advance position by child's border-box width
currentMainPos = currentMainPos + child:getBorderBoxWidth() + itemSpacing
else
-- Vertical layout: main axis is Y, cross axis is X
-- Position child at border box (x, y represents top-left including padding)
-- Add reservedMainStart to account for absolutely positioned siblings
child.y = self.y + self.padding.top + reservedMainStart + currentMainPos
-- BORDER-BOX MODEL: Use border-box dimensions for alignment calculations
local childBorderBoxWidth = child:getBorderBoxWidth()
if effectiveAlign == AlignItems.FLEX_START then
child.x = self.x + self.padding.left + reservedCrossStart + currentCrossPos
elseif effectiveAlign == AlignItems.CENTER then
local childTotalWidth = (child.width or 0) + child.padding.left + child.padding.right
child.x = self.x + self.padding.left + reservedCrossStart + currentCrossPos + ((lineHeight - childTotalWidth) / 2)
child.x = self.x
+ self.padding.left
+ reservedCrossStart
+ currentCrossPos
+ ((lineHeight - childBorderBoxWidth) / 2)
elseif effectiveAlign == AlignItems.FLEX_END then
local childTotalWidth = (child.width or 0) + child.padding.left + child.padding.right
child.x = self.x + self.padding.left + reservedCrossStart + currentCrossPos + lineHeight - childTotalWidth
child.x = self.x + self.padding.left + reservedCrossStart + currentCrossPos + lineHeight - childBorderBoxWidth
elseif effectiveAlign == AlignItems.STRETCH then
-- STRETCH always stretches children in cross-axis direction
child.width = lineHeight - child.padding.left - child.padding.right
-- STRETCH: Set border-box width to lineHeight, content area shrinks to fit
child._borderBoxWidth = lineHeight
child.width = math.max(0, lineHeight - child.padding.left - child.padding.right)
child.x = self.x + self.padding.left + reservedCrossStart + currentCrossPos
end
@@ -2403,9 +2438,8 @@ function Element:layoutChildren()
child:layoutChildren()
end
-- Advance position by child's total height (height + padding)
local childTotalHeight = (child.height or 0) + child.padding.top + child.padding.bottom
currentMainPos = currentMainPos + childTotalHeight + itemSpacing
-- Advance position by child's border-box height
currentMainPos = currentMainPos + child:getBorderBoxHeight() + itemSpacing
end
end
@@ -2466,6 +2500,7 @@ function Element:draw()
-- LAYER 1: Draw backgroundColor first (behind everything)
-- Apply opacity to all drawing operations
-- (x, y) represents border box, so draw background from (x, y)
-- BORDER-BOX MODEL: Use stored border-box dimensions for drawing
local backgroundWithOpacity =
Color.new(drawBackgroundColor.r, drawBackgroundColor.g, drawBackgroundColor.b, drawBackgroundColor.a * self.opacity)
love.graphics.setColor(backgroundWithOpacity:toRGBA())
@@ -2473,8 +2508,8 @@ function Element:draw()
"fill",
self.x,
self.y,
self.width + self.padding.left + self.padding.right,
self.height + self.padding.top + self.padding.bottom,
self._borderBoxWidth or (self.width + self.padding.left + self.padding.right),
self._borderBoxHeight or (self.height + self.padding.top + self.padding.bottom),
self.cornerRadius
)
@@ -2512,15 +2547,7 @@ function Element:draw()
local atlasToUse = component._loadedAtlas or themeToUse.atlas
if atlasToUse then
NineSlice.draw(
component,
atlasToUse,
self.x,
self.y,
self.width + self.padding.left + self.padding.right,
self.height + self.padding.top + self.padding.bottom,
self.opacity
)
NineSlice.draw(component, atlasToUse, self.x, self.y, self.width, self.height, self.padding, self.opacity)
else
print("[FlexLove] No atlas for component: " .. self.themeComponent)
end
@@ -2540,39 +2567,26 @@ function Element:draw()
-- Check if all borders are enabled
local allBorders = self.border.top and self.border.bottom and self.border.left and self.border.right
-- BORDER-BOX MODEL: Use stored border-box dimensions for drawing
local borderBoxWidth = self._borderBoxWidth or (self.width + self.padding.left + self.padding.right)
local borderBoxHeight = self._borderBoxHeight or (self.height + self.padding.top + self.padding.bottom)
if allBorders then
-- Draw complete rounded rectangle border
RoundedRect.draw(
"line",
self.x,
self.y,
self.width + self.padding.left + self.padding.right,
self.height + self.padding.top + self.padding.bottom,
self.cornerRadius
)
RoundedRect.draw("line", self.x, self.y, borderBoxWidth, borderBoxHeight, self.cornerRadius)
else
-- Draw individual borders (without rounded corners for partial borders)
if self.border.top then
love.graphics.line(self.x, self.y, self.x + self.width + self.padding.left + self.padding.right, self.y)
love.graphics.line(self.x, self.y, self.x + borderBoxWidth, self.y)
end
if self.border.bottom then
love.graphics.line(
self.x,
self.y + self.height + self.padding.top + self.padding.bottom,
self.x + self.width + self.padding.left + self.padding.right,
self.y + self.height + self.padding.top + self.padding.bottom
)
love.graphics.line(self.x, self.y + borderBoxHeight, self.x + borderBoxWidth, self.y + borderBoxHeight)
end
if self.border.left then
love.graphics.line(self.x, self.y, self.x, self.y + self.height + self.padding.top + self.padding.bottom)
love.graphics.line(self.x, self.y, self.x, self.y + borderBoxHeight)
end
if self.border.right then
love.graphics.line(
self.x + self.width + self.padding.left + self.padding.right,
self.y,
self.x + self.width + self.padding.left + self.padding.right,
self.y + self.height + self.padding.top + self.padding.bottom
)
love.graphics.line(self.x + borderBoxWidth, self.y, self.x + borderBoxWidth, self.y + borderBoxHeight)
end
end
@@ -2645,15 +2659,11 @@ function Element:draw()
end
end
if anyPressed then
-- BORDER-BOX MODEL: Use stored border-box dimensions for drawing
local borderBoxWidth = self._borderBoxWidth or (self.width + self.padding.left + self.padding.right)
local borderBoxHeight = self._borderBoxHeight or (self.height + self.padding.top + self.padding.bottom)
love.graphics.setColor(0.5, 0.5, 0.5, 0.3 * self.opacity) -- Semi-transparent gray for pressed state with opacity
RoundedRect.draw(
"fill",
self.x,
self.y,
self.width + self.padding.left + self.padding.right,
self.height + self.padding.top + self.padding.bottom,
self.cornerRadius
)
RoundedRect.draw("fill", self.x, self.y, borderBoxWidth, borderBoxHeight, self.cornerRadius)
end
end
@@ -2674,13 +2684,10 @@ function Element:draw()
if hasRoundedCorners and #sortedChildren > 0 then
-- Use stencil to clip children to rounded rectangle
local stencilFunc = RoundedRect.stencilFunction(
self.x,
self.y,
self.width + self.padding.left + self.padding.right,
self.height + self.padding.top + self.padding.bottom,
self.cornerRadius
)
-- BORDER-BOX MODEL: Use stored border-box dimensions for clipping
local borderBoxWidth = self._borderBoxWidth or (self.width + self.padding.left + self.padding.right)
local borderBoxHeight = self._borderBoxHeight or (self.height + self.padding.top + self.padding.bottom)
local stencilFunc = RoundedRect.stencilFunction(self.x, self.y, borderBoxWidth, borderBoxHeight, self.cornerRadius)
love.graphics.stencil(stencilFunc, "replace", 1)
love.graphics.setStencilTest("greater", 0)
@@ -2727,10 +2734,11 @@ function Element:update(dt)
if self.callback or self.themeComponent then
local mx, my = love.mouse.getPosition()
-- Clickable area is the border box (x, y already includes padding)
-- BORDER-BOX MODEL: Use stored border-box dimensions for hit detection
local bx = self.x
local by = self.y
local bw = self.width + self.padding.left + self.padding.right
local bh = self.height + self.padding.top + self.padding.bottom
local bw = self._borderBoxWidth or (self.width + self.padding.left + self.padding.right)
local bh = self._borderBoxHeight or (self.height + self.padding.top + self.padding.bottom)
local isHovering = mx >= bx and mx <= bx + bw and my >= by and my <= by + bh
-- Update theme state based on interaction
@@ -3027,6 +3035,17 @@ function Element:recalculateUnits(newViewportWidth, newViewportHeight)
)
end
end
-- BORDER-BOX MODEL: After recalculating width/height/padding, update border-box dimensions
-- Width and height were calculated as border-box, now we need to subtract padding for content area
if self.units.width.unit ~= "auto" then
self._borderBoxWidth = self.width
self.width = math.max(0, self.width - self.padding.left - self.padding.right)
end
if self.units.height.unit ~= "auto" then
self._borderBoxHeight = self.height
self.height = math.max(0, self.height - self.padding.top - self.padding.bottom)
end
end
--- Resize element and its children based on game window size change
@@ -3042,10 +3061,16 @@ function Element:resize(newGameWidth, newGameHeight)
-- Recalculate auto-sized dimensions after children are resized
if self.autosizing.width then
self.width = self:calculateAutoWidth()
local contentWidth = self:calculateAutoWidth()
-- BORDER-BOX MODEL: Add padding to get border-box, then subtract to get content
self._borderBoxWidth = contentWidth + self.padding.left + self.padding.right
self.width = contentWidth
end
if self.autosizing.height then
self.height = self:calculateAutoHeight()
local contentHeight = self:calculateAutoHeight()
-- BORDER-BOX MODEL: Add padding to get border-box, then subtract to get content
self._borderBoxHeight = contentHeight + self.padding.top + self.padding.bottom
self.height = contentHeight
end
self:layoutChildren()
@@ -3089,21 +3114,20 @@ function Element:calculateTextHeight()
end
function Element:calculateAutoWidth()
local width = self:calculateTextWidth()
-- BORDER-BOX MODEL: Calculate content width, caller will add padding to get border-box
local contentWidth = self:calculateTextWidth()
if not self.children or #self.children == 0 then
return width
return contentWidth
end
local totalWidth = width
local totalWidth = 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
local paddingAdjustment = (child.padding.left or 0) + (child.padding.right or 0)
local childWidth = child.width or child:calculateAutoWidth()
local childOffset = childWidth + paddingAdjustment
totalWidth = totalWidth + childOffset
-- BORDER-BOX MODEL: Use border-box width for auto-sizing calculations
local childBorderBoxWidth = child:getBorderBoxWidth()
totalWidth = totalWidth + childBorderBoxWidth
participatingChildren = participatingChildren + 1
end
end
@@ -3123,11 +3147,9 @@ function Element:calculateAutoHeight()
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
local paddingAdjustment = (child.padding.top or 0) + (child.padding.bottom or 0)
local childHeight = child.height or child:calculateAutoHeight()
local childOffset = childHeight + paddingAdjustment
totalHeight = totalHeight + childOffset
-- BORDER-BOX MODEL: Use border-box height for auto-sizing calculations
local childBorderBoxHeight = child:getBorderBoxHeight()
totalHeight = totalHeight + childBorderBoxHeight
participatingChildren = participatingChildren + 1
end
end

View File

@@ -0,0 +1,284 @@
local FlexLove = require("FlexLove")
local Gui = FlexLove.GUI
local Theme = FlexLove.Theme
local Color = FlexLove.Color
---@class ProportionalScalingDemo
---@field window Element
local ProportionalScalingDemo = {}
ProportionalScalingDemo.__index = ProportionalScalingDemo
function ProportionalScalingDemo.init()
local self = setmetatable({}, ProportionalScalingDemo)
-- Load space theme
Theme.load("space")
Theme.setActive("space")
-- Create main demo window
self.window = Gui.new({
x = 50,
y = 50,
width = 900,
height = 700,
backgroundColor = Color.new(0.1, 0.1, 0.15, 0.95),
positioning = "flex",
flexDirection = "vertical",
gap = 20,
padding = { top = 20, right = 20, bottom = 20, left = 20 },
})
-- Title
Gui.new({
parent = self.window,
height = 40,
text = "Proportional 9-Slice Scaling Demo",
textSize = 24,
textAlign = "center",
textColor = Color.new(1, 1, 1, 1),
backgroundColor = Color.new(0.2, 0.2, 0.3, 1),
})
-- Description
Gui.new({
parent = self.window,
height = 80,
text = "Theme borders render ONLY in the padding area!\nwidth/height = content area, padding = border thickness\nBorders scale to fit padding dimensions.",
textSize = 14,
textAlign = "center",
textColor = Color.new(0.8, 0.9, 1, 1),
backgroundColor = Color.new(0.15, 0.15, 0.2, 0.8),
padding = { top = 10, right = 10, bottom = 10, left = 10 },
})
-- Small buttons section
local smallSection = Gui.new({
parent = self.window,
height = 160,
positioning = "flex",
flexDirection = "vertical",
gap = 10,
backgroundColor = Color.new(0.12, 0.12, 0.17, 0.5),
padding = { top = 15, right = 15, bottom = 15, left = 15 },
})
Gui.new({
parent = smallSection,
height = 20,
text = "Different Padding Sizes (borders scale to padding)",
textSize = 14,
textColor = Color.new(0.8, 0.9, 1, 1),
})
local smallButtonRow = Gui.new({
parent = smallSection,
positioning = "flex",
flexDirection = "horizontal",
gap = 15,
justifyContent = "center",
alignItems = "center",
})
-- Buttons with different padding - borders scale to fit
Gui.new({
parent = smallButtonRow,
text = "Thin Border",
textAlign = "center",
textColor = Color.new(1, 1, 1, 1),
padding = { horizontal = 8, vertical = 4 },
themeComponent = "button",
callback = function()
print("Thin border button clicked!")
end,
})
Gui.new({
parent = smallButtonRow,
text = "Medium Border",
textAlign = "center",
textColor = Color.new(1, 1, 1, 1),
padding = { horizontal = 16, vertical = 8 },
themeComponent = "button",
callback = function()
print("Medium border button clicked!")
end,
})
Gui.new({
parent = smallButtonRow,
text = "Thick Border",
textAlign = "center",
textColor = Color.new(1, 1, 1, 1),
padding = { horizontal = 24, vertical = 12 },
themeComponent = "button",
callback = function()
print("Thick border button clicked!")
end,
})
Gui.new({
parent = smallButtonRow,
text = "Extra Thick",
textAlign = "center",
textColor = Color.new(1, 1, 1, 1),
padding = { horizontal = 32, vertical = 16 },
themeComponent = "button",
callback = function()
print("Extra thick border button clicked!")
end,
})
-- Content area demonstration
local contentSection = Gui.new({
parent = self.window,
height = 180,
positioning = "flex",
flexDirection = "vertical",
gap = 10,
backgroundColor = Color.new(0.12, 0.12, 0.17, 0.5),
padding = { top = 15, right = 15, bottom = 15, left = 15 },
})
Gui.new({
parent = contentSection,
height = 20,
text = "Content Area = width x height (padding adds border space)",
textSize = 14,
textColor = Color.new(0.8, 0.9, 1, 1),
})
local contentRow = Gui.new({
parent = contentSection,
positioning = "flex",
flexDirection = "horizontal",
gap = 15,
justifyContent = "center",
alignItems = "center",
})
-- Same content size, different padding
Gui.new({
parent = contentRow,
width = 100,
height = 40,
text = "100x40\n+5px pad",
textAlign = "center",
textColor = Color.new(1, 1, 1, 1),
textSize = 10,
padding = { horizontal = 5, vertical = 5 },
themeComponent = "button",
callback = function()
print("Small padding clicked!")
end,
})
Gui.new({
parent = contentRow,
width = 100,
height = 40,
text = "100x40\n+15px pad",
textAlign = "center",
textColor = Color.new(1, 1, 1, 1),
textSize = 10,
padding = { horizontal = 15, vertical = 15 },
themeComponent = "button",
callback = function()
print("Large padding clicked!")
end,
})
Gui.new({
parent = contentRow,
width = 100,
height = 40,
text = "100x40\n+25px pad",
textAlign = "center",
textColor = Color.new(1, 1, 1, 1),
textSize = 10,
padding = { horizontal = 25, vertical = 25 },
themeComponent = "button",
callback = function()
print("Extra large padding clicked!")
end,
})
-- Panel section
local panelSection = Gui.new({
parent = self.window,
height = 250,
positioning = "flex",
flexDirection = "vertical",
gap = 10,
backgroundColor = Color.new(0.12, 0.12, 0.17, 0.5),
padding = { top = 15, right = 15, bottom = 15, left = 15 },
})
Gui.new({
parent = panelSection,
height = 20,
text = "Themed Panels (different sizes)",
textSize = 14,
textColor = Color.new(0.8, 0.9, 1, 1),
})
local panelRow = Gui.new({
parent = panelSection,
positioning = "flex",
flexDirection = "horizontal",
gap = 15,
justifyContent = "center",
alignItems = "flex-start",
})
-- Small panel
local smallPanel = Gui.new({
parent = panelRow,
width = 150,
height = 100,
themeComponent = "panel",
padding = { top = 15, right = 15, bottom = 15, left = 15 },
})
Gui.new({
parent = smallPanel,
text = "Small\nPanel",
textAlign = "center",
textColor = Color.new(1, 1, 1, 1),
})
-- Medium panel
local mediumPanel = Gui.new({
parent = panelRow,
width = 200,
height = 150,
themeComponent = "panel",
padding = { top = 20, right = 20, bottom = 20, left = 20 },
})
Gui.new({
parent = mediumPanel,
text = "Medium Panel\nwith more content",
textAlign = "center",
textColor = Color.new(1, 1, 1, 1),
})
-- Large panel
local largePanel = Gui.new({
parent = panelRow,
width = 250,
height = 180,
themeComponent = "panel",
padding = { top = 25, right = 25, bottom = 25, left = 25 },
})
Gui.new({
parent = largePanel,
text = "Large Panel\nScales proportionally\nBorders maintain aspect",
textAlign = "center",
textColor = Color.new(1, 1, 1, 1),
})
return self
end
return ProportionalScalingDemo.init()

View File

@@ -51,8 +51,8 @@ function TestAbsolutePositioningBasic:testDefaultAbsolutePositioning()
height = 100,
})
-- Default should be absolute positioning (RELATIVE not yet implemented)
luaunit.assertEquals(elem.positioning, Positioning.ABSOLUTE)
-- Default should be relative positioning
luaunit.assertEquals(elem.positioning, Positioning.RELATIVE)
luaunit.assertEquals(elem.x, 50)
luaunit.assertEquals(elem.y, 75)
end

View File

@@ -126,7 +126,7 @@ function TestLayoutValidation:testMissingRequiredPropertiesDefaults()
luaunit.assertIsNumber(element.y)
luaunit.assertIsNumber(element.width)
luaunit.assertIsNumber(element.height)
luaunit.assertEquals(element.positioning, Positioning.ABSOLUTE) -- Default positioning
luaunit.assertEquals(element.positioning, Positioning.RELATIVE) -- Default positioning
-- Test flex container with minimal properties
local success2, flex_element = captureError(function()