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

View File

@@ -126,7 +126,7 @@ function TestLayoutValidation:testMissingRequiredPropertiesDefaults()
luaunit.assertIsNumber(element.y) luaunit.assertIsNumber(element.y)
luaunit.assertIsNumber(element.width) luaunit.assertIsNumber(element.width)
luaunit.assertIsNumber(element.height) 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 -- Test flex container with minimal properties
local success2, flex_element = captureError(function() local success2, flex_element = captureError(function()