idk
This commit is contained in:
544
FlexLove.lua
544
FlexLove.lua
@@ -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,10 +1465,13 @@ 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
|
||||
if self.parent then
|
||||
self.fontFamily = self.parent.fontFamily
|
||||
else
|
||||
self.fontFamily = "default"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Handle textSize BEFORE width/height calculation (needed for auto-sizing)
|
||||
if props.textSize then
|
||||
@@ -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
|
||||
|
||||
284
examples/ProportionalScalingDemo.lua
Normal file
284
examples/ProportionalScalingDemo.lua
Normal 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()
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user