start grid impl

This commit is contained in:
Michael Freno
2025-10-10 22:18:30 -04:00
parent 67e32af78d
commit da3581785f
5 changed files with 1434 additions and 6 deletions

View File

@@ -31,7 +31,7 @@ local enums = {
---@enum TextAlign
TextAlign = { START = "start", CENTER = "center", END = "end", JUSTIFY = "justify" },
---@enum Positioning
Positioning = { ABSOLUTE = "absolute", RELATIVE = "relative", FLEX = "flex" },
Positioning = { ABSOLUTE = "absolute", RELATIVE = "relative", FLEX = "flex", GRID = "grid" },
---@enum FlexDirection
FlexDirection = { HORIZONTAL = "horizontal", VERTICAL = "vertical" },
---@enum JustifyContent
@@ -81,9 +81,38 @@ local enums = {
},
---@enum FlexWrap
FlexWrap = { NOWRAP = "nowrap", WRAP = "wrap", WRAP_REVERSE = "wrap-reverse" },
---@enum GridAutoFlow
GridAutoFlow = { ROW = "row", COLUMN = "column", ROW_DENSE = "row dense", COLUMN_DENSE = "column dense" },
---@enum JustifyItems
JustifyItems = {
STRETCH = "stretch",
START = "start",
END = "end",
CENTER = "center",
},
---@enum AlignContent (Grid)
GridAlignContent = {
STRETCH = "stretch",
START = "start",
END = "end",
CENTER = "center",
SPACE_BETWEEN = "space-between",
SPACE_AROUND = "space-around",
SPACE_EVENLY = "space-evenly",
},
---@enum JustifyContent (Grid)
GridJustifyContent = {
STRETCH = "stretch",
START = "start",
END = "end",
CENTER = "center",
SPACE_BETWEEN = "space-between",
SPACE_AROUND = "space-around",
SPACE_EVENLY = "space-evenly",
},
}
local Positioning, FlexDirection, JustifyContent, AlignContent, AlignItems, TextAlign, AlignSelf, JustifySelf, FlexWrap =
local Positioning, FlexDirection, JustifyContent, AlignContent, AlignItems, TextAlign, AlignSelf, JustifySelf, FlexWrap, GridAutoFlow, JustifyItems, GridAlignContent, GridJustifyContent =
enums.Positioning,
enums.FlexDirection,
enums.JustifyContent,
@@ -92,7 +121,11 @@ local Positioning, FlexDirection, JustifyContent, AlignContent, AlignItems, Text
enums.TextAlign,
enums.AlignSelf,
enums.JustifySelf,
enums.FlexWrap
enums.FlexWrap,
enums.GridAutoFlow,
enums.JustifyItems,
enums.GridAlignContent,
enums.GridJustifyContent
-- ====================
-- Units System
@@ -243,6 +276,429 @@ function Units.resolveSpacing(spacingProps, parentWidth, parentHeight)
return result
end
-- ====================
-- Grid System
-- ====================
--- Grid track parsing and layout calculations
local Grid = {}
--- Parse a single track size value
---@param trackSize string|number
---@return table -- { type: "px"|"fr"|"%"|"auto"|"minmax"|"min-content"|"max-content", value: number?, min: number?, max: number? }
function Grid.parseTrackSize(trackSize)
if type(trackSize) == "number" then
return { type = "px", value = trackSize }
end
if type(trackSize) ~= "string" then
return { type = "auto" }
end
-- Handle auto
if trackSize == "auto" then
return { type = "auto" }
end
-- Handle min-content and max-content
if trackSize == "min-content" then
return { type = "min-content" }
end
if trackSize == "max-content" then
return { type = "max-content" }
end
-- Handle fr units
local frValue = trackSize:match("^([%d%.]+)fr$")
if frValue then
return { type = "fr", value = tonumber(frValue) }
end
-- Handle percentage
local percentValue = trackSize:match("^([%d%.]+)%%$")
if percentValue then
return { type = "%", value = tonumber(percentValue) }
end
-- Handle pixel values
local pxValue = trackSize:match("^([%d%.]+)px$")
if pxValue then
return { type = "px", value = tonumber(pxValue) }
end
-- Handle minmax(min, max)
local minStr, maxStr = trackSize:match("^minmax%s*%(([^,]+),%s*([^)]+)%)$")
if minStr and maxStr then
local minTrack = Grid.parseTrackSize(minStr:match("^%s*(.-)%s*$"))
local maxTrack = Grid.parseTrackSize(maxStr:match("^%s*(.-)%s*$"))
return { type = "minmax", min = minTrack, max = maxTrack }
end
-- Default to auto for unrecognized formats
return { type = "auto" }
end
--- Parse track list (e.g., "1fr 2fr 100px" or "repeat(3, 1fr)")
---@param trackList string|table
---@return table -- Array of parsed track sizes
function Grid.parseTrackList(trackList)
if type(trackList) == "table" then
local result = {}
for _, track in ipairs(trackList) do
table.insert(result, Grid.parseTrackSize(track))
end
return result
end
if type(trackList) ~= "string" then
return {}
end
local tracks = {}
-- Handle repeat() function
local repeatMatch = trackList:match("^repeat%s*%(([^)]+)%)$")
if repeatMatch then
local countStr, pattern = repeatMatch:match("^%s*(%d+)%s*,%s*(.+)%s*$")
if countStr and pattern then
local count = tonumber(countStr)
local repeatTracks = Grid.parseTrackList(pattern)
for i = 1, count do
for _, track in ipairs(repeatTracks) do
table.insert(tracks, track)
end
end
return tracks
end
end
-- Split by whitespace and parse each track
for trackStr in trackList:gmatch("%S+") do
table.insert(tracks, Grid.parseTrackSize(trackStr))
end
return tracks
end
--- Resolve track sizes to actual pixel values
---@param tracks table -- Array of parsed track sizes
---@param availableSize number -- Available space in pixels
---@param gap number -- Gap between tracks
---@return table -- Array of resolved pixel sizes
function Grid.resolveTrackSizes(tracks, availableSize, gap)
if #tracks == 0 then
return {}
end
-- Calculate total gap space
local totalGapSize = (#tracks - 1) * gap
local remainingSpace = availableSize - totalGapSize
local resolvedSizes = {}
local frTracks = {}
local frTotal = 0
-- First pass: resolve fixed sizes and collect fr tracks
for i, track in ipairs(tracks) do
if track.type == "px" then
resolvedSizes[i] = track.value
remainingSpace = remainingSpace - track.value
elseif track.type == "%" then
local size = (track.value / 100) * availableSize
resolvedSizes[i] = size
remainingSpace = remainingSpace - size
elseif track.type == "fr" then
table.insert(frTracks, i)
frTotal = frTotal + track.value
elseif track.type == "auto" or track.type == "min-content" or track.type == "max-content" then
-- For now, treat auto/min-content/max-content as equal distribution of remaining space
resolvedSizes[i] = 0 -- Will be calculated in second pass
elseif track.type == "minmax" then
-- Simplified: use max if available, otherwise min
-- This is a basic implementation - full minmax is complex
if track.max.type == "fr" then
table.insert(frTracks, i)
frTotal = frTotal + track.max.value
else
local maxSize = Grid.parseTrackSize(track.max)
if maxSize.type == "px" then
resolvedSizes[i] = maxSize.value
remainingSpace = remainingSpace - maxSize.value
else
resolvedSizes[i] = 0
end
end
end
end
-- Second pass: distribute remaining space to fr tracks
if frTotal > 0 and remainingSpace > 0 then
local frUnit = remainingSpace / frTotal
for _, i in ipairs(frTracks) do
local track = tracks[i]
if track.type == "fr" then
resolvedSizes[i] = track.value * frUnit
elseif track.type == "minmax" and track.max.type == "fr" then
resolvedSizes[i] = track.max.value * frUnit
end
end
else
-- No space left for fr tracks
for _, i in ipairs(frTracks) do
resolvedSizes[i] = 0
end
end
-- Third pass: handle auto tracks (equal distribution of any remaining space)
local autoTracks = {}
for i, track in ipairs(tracks) do
if track.type == "auto" or track.type == "min-content" or track.type == "max-content" then
if resolvedSizes[i] == 0 then
table.insert(autoTracks, i)
end
end
end
if #autoTracks > 0 then
local autoSize = math.max(0, remainingSpace / #autoTracks)
for _, i in ipairs(autoTracks) do
resolvedSizes[i] = autoSize
end
end
return resolvedSizes
end
--- Parse grid line placement (e.g., "1", "2 / 4", "span 2")
---@param placement string|number|nil
---@return table -- { start: number?, end: number?, span: number? }
function Grid.parsePlacement(placement)
if not placement then
return { start = nil, end_ = nil, span = nil }
end
if type(placement) == "number" then
return { start = placement, end_ = nil, span = nil }
end
if type(placement) ~= "string" then
return { start = nil, end_ = nil, span = nil }
end
-- Handle "span N" format
local spanValue = placement:match("^span%s+(%d+)$")
if spanValue then
return { start = nil, end_ = nil, span = tonumber(spanValue) }
end
-- Handle "start / end" format
local startStr, endStr = placement:match("^(%d+)%s*/%s*(%d+)$")
if startStr and endStr then
return { start = tonumber(startStr), end_ = tonumber(endStr), span = nil }
end
-- Handle single number
local lineNum = tonumber(placement)
if lineNum then
return { start = lineNum, end_ = nil, span = nil }
end
return { start = nil, end_ = nil, span = nil }
end
--- Calculate grid item placement
---@param item Element
---@param columnCount number
---@param rowCount number
---@param autoPlacementCursor {column: number, row: number}
---@param gridAutoFlow string
---@return table -- { columnStart: number, columnEnd: number, rowStart: number, rowEnd: number }
function Grid.calculateItemPlacement(item, columnCount, rowCount, autoPlacementCursor, gridAutoFlow)
local columnPlacement = Grid.parsePlacement(item.gridColumn)
local rowPlacement = Grid.parsePlacement(item.gridRow)
local columnStart, columnEnd, rowStart, rowEnd
-- Determine column placement
if columnPlacement.start and columnPlacement.end_ then
columnStart = columnPlacement.start
columnEnd = columnPlacement.end_
elseif columnPlacement.start and columnPlacement.span then
columnStart = columnPlacement.start
columnEnd = columnStart + columnPlacement.span
elseif columnPlacement.start then
columnStart = columnPlacement.start
columnEnd = columnStart + 1
elseif columnPlacement.span then
-- Auto-place with span
columnStart = autoPlacementCursor.column
columnEnd = columnStart + columnPlacement.span
else
-- Auto-place
columnStart = autoPlacementCursor.column
columnEnd = columnStart + 1
end
-- Determine row placement
if rowPlacement.start and rowPlacement.end_ then
rowStart = rowPlacement.start
rowEnd = rowPlacement.end_
elseif rowPlacement.start and rowPlacement.span then
rowStart = rowPlacement.start
rowEnd = rowStart + rowPlacement.span
elseif rowPlacement.start then
rowStart = rowPlacement.start
rowEnd = rowStart + 1
elseif rowPlacement.span then
-- Auto-place with span
rowStart = autoPlacementCursor.row
rowEnd = rowStart + rowPlacement.span
else
-- Auto-place
rowStart = autoPlacementCursor.row
rowEnd = rowStart + 1
end
return {
columnStart = columnStart,
columnEnd = columnEnd,
rowStart = rowStart,
rowEnd = rowEnd,
}
end
--- Layout grid items within a grid container
---@param element Element -- Grid container element
function Grid.layoutGridItems(element)
if not element.gridTemplateColumns and not element.gridTemplateRows then
-- No grid template defined, fall back to single column/row
element.gridTemplateColumns = element.gridTemplateColumns or "1fr"
element.gridTemplateRows = element.gridTemplateRows or "auto"
end
-- Parse track definitions
local columnTracks = Grid.parseTrackList(element.gridTemplateColumns or "1fr")
local rowTracks = Grid.parseTrackList(element.gridTemplateRows or "auto")
-- Calculate available space
local availableWidth = element.width - element.padding.left - element.padding.right
local availableHeight = element.height - element.padding.top - element.padding.bottom
-- Resolve track sizes
local columnGap = element.columnGap or 0
local rowGap = element.rowGap or 0
local columnSizes = Grid.resolveTrackSizes(columnTracks, availableWidth, columnGap)
local rowSizes = Grid.resolveTrackSizes(rowTracks, availableHeight, rowGap)
-- Calculate column and row positions
local columnPositions = {}
local rowPositions = {}
local currentX = element.x + element.padding.left
for i, size in ipairs(columnSizes) do
columnPositions[i] = currentX
currentX = currentX + size + columnGap
end
columnPositions[#columnSizes + 1] = currentX - columnGap -- End position
local currentY = element.y + element.padding.top
for i, size in ipairs(rowSizes) do
rowPositions[i] = currentY
currentY = currentY + size + rowGap
end
rowPositions[#rowSizes + 1] = currentY - rowGap -- End position
-- Auto-placement cursor
local autoPlacementCursor = { column = 1, row = 1 }
local gridAutoFlow = element.gridAutoFlow or GridAutoFlow.ROW
-- Place grid items
for _, child in ipairs(element.children) do
-- Skip explicitly absolute positioned children
if not (child.positioning == Positioning.ABSOLUTE and child._explicitlyAbsolute) then
local placement = Grid.calculateItemPlacement(
child,
#columnSizes,
#rowSizes,
autoPlacementCursor,
gridAutoFlow
)
-- Ensure placement is within bounds, expand grid if necessary
local columnStart = math.max(1, math.min(placement.columnStart, #columnSizes + 1))
local columnEnd = math.max(columnStart + 1, math.min(placement.columnEnd, #columnSizes + 1))
local rowStart = math.max(1, math.min(placement.rowStart, #rowSizes + 1))
local rowEnd = math.max(rowStart + 1, math.min(placement.rowEnd, #rowSizes + 1))
-- Calculate item position and size
local itemX = columnPositions[columnStart] or element.x
local itemY = rowPositions[rowStart] or element.y
local itemWidth = (columnPositions[columnEnd] or (element.x + element.width)) - itemX - columnGap
local itemHeight = (rowPositions[rowEnd] or (element.y + element.height)) - itemY - rowGap
-- Apply alignment within grid cell
local effectiveJustifySelf = child.justifySelf or element.justifyItems or JustifyItems.STRETCH
local effectiveAlignSelf = child.alignSelf or element.alignItems or AlignItems.STRETCH
-- Handle justifySelf (horizontal alignment)
if effectiveJustifySelf == JustifyItems.STRETCH or effectiveJustifySelf == "stretch" then
child.x = itemX + child.padding.left
child.width = itemWidth - child.padding.left - child.padding.right
elseif effectiveJustifySelf == JustifyItems.START or effectiveJustifySelf == "start" or effectiveJustifySelf == "flex-start" then
child.x = itemX + child.padding.left
-- Keep child's natural width
elseif effectiveJustifySelf == JustifyItems.END or effectiveJustifySelf == "end" or effectiveJustifySelf == "flex-end" then
child.x = itemX + itemWidth - child.width - child.padding.right
elseif effectiveJustifySelf == JustifyItems.CENTER or effectiveJustifySelf == "center" then
child.x = itemX + (itemWidth - child.width) / 2
else
-- Default to stretch
child.x = itemX + child.padding.left
child.width = itemWidth - child.padding.left - child.padding.right
end
-- Handle alignSelf (vertical alignment)
if effectiveAlignSelf == AlignItems.STRETCH or effectiveAlignSelf == "stretch" then
child.y = itemY + child.padding.top
child.height = itemHeight - child.padding.top - child.padding.bottom
elseif effectiveAlignSelf == AlignItems.FLEX_START or effectiveAlignSelf == "flex-start" or effectiveAlignSelf == "start" then
child.y = itemY + child.padding.top
-- Keep child's natural height
elseif effectiveAlignSelf == AlignItems.FLEX_END or effectiveAlignSelf == "flex-end" or effectiveAlignSelf == "end" then
child.y = itemY + itemHeight - child.height - child.padding.bottom
elseif effectiveAlignSelf == AlignItems.CENTER or effectiveAlignSelf == "center" then
child.y = itemY + (itemHeight - child.height) / 2
else
-- Default to stretch
child.y = itemY + child.padding.top
child.height = itemHeight - child.padding.top - child.padding.bottom
end
-- Update auto-placement cursor
if gridAutoFlow == GridAutoFlow.ROW or gridAutoFlow == "row" then
autoPlacementCursor.column = columnEnd
if autoPlacementCursor.column > #columnSizes then
autoPlacementCursor.column = 1
autoPlacementCursor.row = autoPlacementCursor.row + 1
end
elseif gridAutoFlow == GridAutoFlow.COLUMN or gridAutoFlow == "column" then
autoPlacementCursor.row = rowEnd
if autoPlacementCursor.row > #rowSizes then
autoPlacementCursor.row = 1
autoPlacementCursor.column = autoPlacementCursor.column + 1
end
end
-- Layout child's children if it has any
if #child.children > 0 then
child:layoutChildren()
end
end
end
end
--- Top level GUI manager
---@class Gui
---@field topElements table<integer, Element>
@@ -516,6 +972,18 @@ end
---@field transition TransitionProps -- Transition settings for animations
---@field callback function? -- Callback function for click events
---@field units table -- Original unit specifications for responsive behavior
---@field gridTemplateColumns string|table? -- Grid column track definitions
---@field gridTemplateRows string|table? -- Grid row track definitions
---@field gridAutoFlow GridAutoFlow? -- Grid auto-placement algorithm
---@field gridAutoColumns string? -- Size of auto-generated columns
---@field gridAutoRows string? -- Size of auto-generated rows
---@field columnGap number|string? -- Gap between grid columns
---@field rowGap number|string? -- Gap between grid rows
---@field gridColumn string|number? -- Grid item column placement
---@field gridRow string|number? -- Grid item row placement
---@field gridArea string? -- Grid item named area placement
---@field justifyItems JustifyItems? -- Default horizontal alignment for grid items
---@field alignItems AlignItems? -- Default vertical alignment for grid items
local Element = {}
Element.__index = Element
@@ -555,6 +1023,17 @@ Element.__index = Element
---@field callback function? -- Callback function for click events
---@field transform table? -- Transform properties for animations and styling
---@field transition table? -- Transition settings for animations
---@field gridTemplateColumns string|table? -- Grid column track definitions (e.g., "1fr 2fr 100px" or {"1fr", "2fr", "100px"})
---@field gridTemplateRows string|table? -- Grid row track definitions
---@field gridAutoFlow GridAutoFlow? -- Grid auto-placement algorithm (default: ROW)
---@field gridAutoColumns string? -- Size of auto-generated columns (default: "auto")
---@field gridAutoRows string? -- Size of auto-generated rows (default: "auto")
---@field columnGap number|string? -- Gap between grid columns
---@field rowGap number|string? -- Gap between grid rows
---@field gridColumn string|number? -- Grid item column placement (e.g., "1", "2 / 4", "span 2")
---@field gridRow string|number? -- Grid item row placement
---@field gridArea string? -- Grid item named area placement
---@field justifyItems JustifyItems? -- Default horizontal alignment for grid items
local ElementProps = {}
---@param props ElementProps
@@ -1023,6 +1502,47 @@ function Element.new(props)
self.justifySelf = props.justifySelf or JustifySelf.AUTO
end
-- Grid container properties
if self.positioning == Positioning.GRID then
self.gridTemplateColumns = props.gridTemplateColumns
self.gridTemplateRows = props.gridTemplateRows
self.gridAutoFlow = props.gridAutoFlow or GridAutoFlow.ROW
self.gridAutoColumns = props.gridAutoColumns or "auto"
self.gridAutoRows = props.gridAutoRows or "auto"
self.justifyContent = props.justifyContent or GridJustifyContent.START
self.alignContent = props.alignContent or GridAlignContent.START
self.justifyItems = props.justifyItems or JustifyItems.STRETCH
self.alignItems = props.alignItems or AlignItems.STRETCH
-- Handle columnGap and rowGap
if props.columnGap then
if type(props.columnGap) == "string" then
local value, unit = Units.parse(props.columnGap)
self.columnGap = Units.resolve(value, unit, viewportWidth, viewportHeight, self.width)
else
self.columnGap = props.columnGap
end
else
self.columnGap = 0
end
if props.rowGap then
if type(props.rowGap) == "string" then
local value, unit = Units.parse(props.rowGap)
self.rowGap = Units.resolve(value, unit, viewportWidth, viewportHeight, self.height)
else
self.rowGap = props.rowGap
end
else
self.rowGap = 0
end
end
-- Grid item properties (can be set on any element that's a child of a grid)
self.gridColumn = props.gridColumn
self.gridRow = props.gridRow
self.gridArea = props.gridArea
self.alignSelf = props.alignSelf or AlignSelf.AUTO
---animation
@@ -1047,9 +1567,9 @@ function Element:addChild(child)
-- If child was created without explicit positioning, inherit from parent
if child._originalPositioning == nil then
-- No explicit positioning was set during construction
if self.positioning == Positioning.FLEX then
child.positioning = Positioning.ABSOLUTE -- They are positioned BY flex, not AS flex
child._explicitlyAbsolute = false -- Participate in parent's flex layout
if self.positioning == Positioning.FLEX or self.positioning == Positioning.GRID then
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
@@ -1116,6 +1636,12 @@ function Element:layoutChildren()
return
end
-- Handle grid layout
if self.positioning == Positioning.GRID then
Grid.layoutGridItems(self)
return
end
local childCount = #self.children
if childCount == 0 then

226
examples/BasicGrid.lua Normal file
View File

@@ -0,0 +1,226 @@
-- Example demonstrating basic CSS Grid layout
-- Shows how to create grid containers and position items
package.path = package.path .. ";?.lua"
require("testing/loveStub")
local FlexLove = require("FlexLove")
local Gui = FlexLove.GUI
local Color = FlexLove.Color
local enums = FlexLove.enums
print("=== Basic Grid Layout Examples ===\n")
-- Example 1: Simple 3-column grid
print("1. Simple 3-Column Grid")
print(" Grid with equal columns using fr units")
local grid1 = Gui.new({
x = 50,
y = 50,
width = 600,
height = 400,
positioning = enums.Positioning.GRID,
gridTemplateColumns = "1fr 1fr 1fr",
gridTemplateRows = "auto auto",
columnGap = 10,
rowGap = 10,
background = Color.new(0.9, 0.9, 0.9, 1),
padding = { horizontal = 20, vertical = 20 },
})
-- Add grid items
for i = 1, 6 do
Gui.new({
parent = grid1,
width = 50,
height = 50,
background = Color.new(0.2, 0.5, 0.8, 1),
text = "Item " .. i,
textColor = Color.new(1, 1, 1, 1),
textAlign = enums.TextAlign.CENTER,
})
end
print(" Grid container: 600x400, 3 columns (1fr each), 2 rows (auto)")
print(" Column gap: 10px, Row gap: 10px")
print(" Items: 6 items auto-placed in grid\n")
-- Example 2: Mixed column sizes
print("2. Mixed Column Sizes")
print(" Grid with different column widths")
Gui.destroy()
local grid2 = Gui.new({
x = 50,
y = 50,
width = 800,
height = 300,
positioning = enums.Positioning.GRID,
gridTemplateColumns = "200px 1fr 2fr",
gridTemplateRows = "100px 100px",
columnGap = 15,
rowGap = 15,
background = Color.new(0.9, 0.9, 0.9, 1),
padding = { horizontal = 20, vertical = 20 },
})
local labels = { "Sidebar", "Content", "Main", "Footer", "Info", "Extra" }
for i = 1, 6 do
Gui.new({
parent = grid2,
background = Color.new(0.3, 0.6, 0.3, 1),
text = labels[i],
textColor = Color.new(1, 1, 1, 1),
textAlign = enums.TextAlign.CENTER,
})
end
print(" Columns: 200px (fixed), 1fr, 2fr (flexible)")
print(" The flexible columns share remaining space proportionally\n")
-- Example 3: Explicit item placement
print("3. Explicit Grid Item Placement")
print(" Items placed at specific grid positions")
Gui.destroy()
local grid3 = Gui.new({
x = 50,
y = 50,
width = 600,
height = 400,
positioning = enums.Positioning.GRID,
gridTemplateColumns = "1fr 1fr 1fr",
gridTemplateRows = "1fr 1fr 1fr",
columnGap = 10,
rowGap = 10,
background = Color.new(0.9, 0.9, 0.9, 1),
padding = { horizontal = 20, vertical = 20 },
})
-- Header spanning all columns
Gui.new({
parent = grid3,
gridColumn = "1 / 4", -- Span from column 1 to 4 (all 3 columns)
gridRow = 1,
background = Color.new(0.8, 0.3, 0.3, 1),
text = "Header (spans all columns)",
textColor = Color.new(1, 1, 1, 1),
textAlign = enums.TextAlign.CENTER,
})
-- Sidebar spanning 2 rows
Gui.new({
parent = grid3,
gridColumn = 1,
gridRow = "2 / 4", -- Span from row 2 to 4 (2 rows)
background = Color.new(0.3, 0.3, 0.8, 1),
text = "Sidebar",
textColor = Color.new(1, 1, 1, 1),
textAlign = enums.TextAlign.CENTER,
})
-- Main content area
Gui.new({
parent = grid3,
gridColumn = "2 / 4", -- Span columns 2-3
gridRow = 2,
background = Color.new(0.3, 0.8, 0.3, 1),
text = "Main Content",
textColor = Color.new(1, 1, 1, 1),
textAlign = enums.TextAlign.CENTER,
})
-- Footer spanning columns 2-3
Gui.new({
parent = grid3,
gridColumn = "2 / 4",
gridRow = 3,
background = Color.new(0.8, 0.8, 0.3, 1),
text = "Footer",
textColor = Color.new(1, 1, 1, 1),
textAlign = enums.TextAlign.CENTER,
})
print(" Header: spans columns 1-3, row 1")
print(" Sidebar: column 1, spans rows 2-3")
print(" Main: spans columns 2-3, row 2")
print(" Footer: spans columns 2-3, row 3\n")
-- Example 4: Using repeat() function
print("4. Using repeat() Function")
print(" Create multiple columns with repeat notation")
Gui.destroy()
local grid4 = Gui.new({
x = 50,
y = 50,
width = 800,
height = 300,
positioning = enums.Positioning.GRID,
gridTemplateColumns = "repeat(4, 1fr)", -- Creates 4 equal columns
gridTemplateRows = "repeat(2, 1fr)", -- Creates 2 equal rows
columnGap = 10,
rowGap = 10,
background = Color.new(0.9, 0.9, 0.9, 1),
padding = { horizontal = 20, vertical = 20 },
})
for i = 1, 8 do
Gui.new({
parent = grid4,
background = Color.new(0.5, 0.3, 0.7, 1),
text = "Box " .. i,
textColor = Color.new(1, 1, 1, 1),
textAlign = enums.TextAlign.CENTER,
})
end
print(" gridTemplateColumns: repeat(4, 1fr)")
print(" gridTemplateRows: repeat(2, 1fr)")
print(" Creates a 4x2 grid with 8 equal cells\n")
-- Example 5: Percentage-based grid
print("5. Percentage-Based Grid")
print(" Using percentage units for columns")
Gui.destroy()
local grid5 = Gui.new({
x = 50,
y = 50,
width = 600,
height = 200,
positioning = enums.Positioning.GRID,
gridTemplateColumns = "25% 50% 25%",
gridTemplateRows = "100%",
columnGap = 0,
rowGap = 0,
background = Color.new(0.9, 0.9, 0.9, 1),
})
local colors = {
Color.new(0.8, 0.2, 0.2, 1),
Color.new(0.2, 0.8, 0.2, 1),
Color.new(0.2, 0.2, 0.8, 1),
}
for i = 1, 3 do
Gui.new({
parent = grid5,
background = colors[i],
text = (i == 1 and "25%" or i == 2 and "50%" or "25%"),
textColor = Color.new(1, 1, 1, 1),
textAlign = enums.TextAlign.CENTER,
})
end
print(" Columns: 25%, 50%, 25%")
print(" Perfect for layouts with specific proportions\n")
print("=== Summary ===")
print("• Set positioning = Positioning.GRID to create a grid container")
print("• Use gridTemplateColumns and gridTemplateRows to define track sizes")
print("• Supported units: px, %, fr, auto, repeat()")
print("• Use columnGap and rowGap for spacing between tracks")
print("• Use gridColumn and gridRow on children for explicit placement")
print("• Use 'start / end' syntax to span multiple tracks")
print("• Items auto-place if no explicit position is set")

249
examples/ResponsiveGrid.lua Normal file
View File

@@ -0,0 +1,249 @@
-- Example demonstrating responsive grid layouts with viewport units
-- Shows how grids adapt to different screen sizes
package.path = package.path .. ";?.lua"
require("testing/loveStub")
local FlexLove = require("FlexLove")
local Gui = FlexLove.GUI
local Color = FlexLove.Color
local enums = FlexLove.enums
print("=== Responsive Grid Layout Examples ===\n")
-- Example 1: Dashboard layout with responsive grid
print("1. Dashboard Layout")
print(" Responsive grid using viewport units")
local dashboard = Gui.new({
x = 0,
y = 0,
width = "100vw",
height = "100vh",
positioning = enums.Positioning.GRID,
gridTemplateColumns = "200px 1fr 1fr",
gridTemplateRows = "60px 1fr 1fr 80px",
columnGap = 10,
rowGap = 10,
background = Color.new(0.95, 0.95, 0.95, 1),
padding = { horizontal = 10, vertical = 10 },
})
-- Header (spans all columns)
Gui.new({
parent = dashboard,
gridColumn = "1 / 4",
gridRow = 1,
background = Color.new(0.2, 0.3, 0.5, 1),
text = "Dashboard Header",
textColor = Color.new(1, 1, 1, 1),
textAlign = enums.TextAlign.CENTER,
textSize = 24,
})
-- Sidebar (spans rows 2-3)
Gui.new({
parent = dashboard,
gridColumn = 1,
gridRow = "2 / 4",
background = Color.new(0.3, 0.3, 0.4, 1),
text = "Navigation",
textColor = Color.new(1, 1, 1, 1),
textAlign = enums.TextAlign.CENTER,
})
-- Main content area (top)
Gui.new({
parent = dashboard,
gridColumn = "2 / 4",
gridRow = 2,
background = Color.new(1, 1, 1, 1),
text = "Main Content",
textColor = Color.new(0.2, 0.2, 0.2, 1),
textAlign = enums.TextAlign.CENTER,
border = { top = true, right = true, bottom = true, left = true },
borderColor = Color.new(0.8, 0.8, 0.8, 1),
})
-- Stats section (bottom left)
Gui.new({
parent = dashboard,
gridColumn = 2,
gridRow = 3,
background = Color.new(0.9, 0.95, 1, 1),
text = "Statistics",
textColor = Color.new(0.2, 0.2, 0.2, 1),
textAlign = enums.TextAlign.CENTER,
border = { top = true, right = true, bottom = true, left = true },
borderColor = Color.new(0.8, 0.8, 0.8, 1),
})
-- Activity feed (bottom right)
Gui.new({
parent = dashboard,
gridColumn = 3,
gridRow = 3,
background = Color.new(1, 0.95, 0.9, 1),
text = "Activity Feed",
textColor = Color.new(0.2, 0.2, 0.2, 1),
textAlign = enums.TextAlign.CENTER,
border = { top = true, right = true, bottom = true, left = true },
borderColor = Color.new(0.8, 0.8, 0.8, 1),
})
-- Footer (spans all columns)
Gui.new({
parent = dashboard,
gridColumn = "1 / 4",
gridRow = 4,
background = Color.new(0.2, 0.3, 0.5, 1),
text = "Footer - Copyright 2025",
textColor = Color.new(1, 1, 1, 1),
textAlign = enums.TextAlign.CENTER,
})
print(" Layout structure:")
print(" - Header: Full width, 60px height")
print(" - Sidebar: 200px wide, spans content rows")
print(" - Main content: Flexible width, top content area")
print(" - Stats & Activity: Split remaining space")
print(" - Footer: Full width, 80px height\n")
-- Example 2: Card grid with auto-flow
print("2. Card Grid with Auto-Flow")
print(" Grid that automatically places items")
Gui.destroy()
local cardGrid = Gui.new({
x = "5vw",
y = "5vh",
width = "90vw",
height = "90vh",
positioning = enums.Positioning.GRID,
gridTemplateColumns = "repeat(3, 1fr)",
gridTemplateRows = "auto",
gridAutoRows = "200px",
gridAutoFlow = enums.GridAutoFlow.ROW,
columnGap = 20,
rowGap = 20,
background = Color.new(0.9, 0.9, 0.9, 1),
padding = { horizontal = 20, vertical = 20 },
})
local cardColors = {
Color.new(0.8, 0.3, 0.3, 1),
Color.new(0.3, 0.8, 0.3, 1),
Color.new(0.3, 0.3, 0.8, 1),
Color.new(0.8, 0.8, 0.3, 1),
Color.new(0.8, 0.3, 0.8, 1),
Color.new(0.3, 0.8, 0.8, 1),
}
for i = 1, 9 do
local colorIndex = ((i - 1) % #cardColors) + 1
Gui.new({
parent = cardGrid,
background = cardColors[colorIndex],
text = "Card " .. i,
textColor = Color.new(1, 1, 1, 1),
textAlign = enums.TextAlign.CENTER,
textSize = 20,
})
end
print(" 9 cards in a 3-column grid")
print(" Auto-flow: ROW (fills rows first)")
print(" Auto-generated rows: 200px each\n")
-- Example 3: Nested grids
print("3. Nested Grid Layout")
print(" Grid containers within grid items")
Gui.destroy()
local outerGrid = Gui.new({
x = 50,
y = 50,
width = 700,
height = 500,
positioning = enums.Positioning.GRID,
gridTemplateColumns = "1fr 2fr",
gridTemplateRows = "1fr 1fr",
columnGap = 15,
rowGap = 15,
background = Color.new(0.85, 0.85, 0.85, 1),
padding = { horizontal = 15, vertical = 15 },
})
-- Top-left: Simple item
Gui.new({
parent = outerGrid,
background = Color.new(0.5, 0.3, 0.7, 1),
text = "Simple Item",
textColor = Color.new(1, 1, 1, 1),
textAlign = enums.TextAlign.CENTER,
})
-- Top-right: Nested grid
local nestedGrid1 = Gui.new({
parent = outerGrid,
positioning = enums.Positioning.GRID,
gridTemplateColumns = "1fr 1fr",
gridTemplateRows = "1fr 1fr",
columnGap = 5,
rowGap = 5,
background = Color.new(0.7, 0.7, 0.7, 1),
padding = { horizontal = 5, vertical = 5 },
})
for i = 1, 4 do
Gui.new({
parent = nestedGrid1,
background = Color.new(0.3, 0.6, 0.9, 1),
text = "A" .. i,
textColor = Color.new(1, 1, 1, 1),
textAlign = enums.TextAlign.CENTER,
})
end
-- Bottom-left: Another nested grid
local nestedGrid2 = Gui.new({
parent = outerGrid,
positioning = enums.Positioning.GRID,
gridTemplateColumns = "repeat(3, 1fr)",
gridTemplateRows = "1fr",
columnGap = 5,
rowGap = 5,
background = Color.new(0.7, 0.7, 0.7, 1),
padding = { horizontal = 5, vertical = 5 },
})
for i = 1, 3 do
Gui.new({
parent = nestedGrid2,
background = Color.new(0.9, 0.6, 0.3, 1),
text = "B" .. i,
textColor = Color.new(1, 1, 1, 1),
textAlign = enums.TextAlign.CENTER,
})
end
-- Bottom-right: Simple item
Gui.new({
parent = outerGrid,
background = Color.new(0.3, 0.7, 0.5, 1),
text = "Another Item",
textColor = Color.new(1, 1, 1, 1),
textAlign = enums.TextAlign.CENTER,
})
print(" Outer grid: 2x2 layout")
print(" Top-right cell: 2x2 nested grid")
print(" Bottom-left cell: 1x3 nested grid")
print(" Other cells: Simple items\n")
print("=== Summary ===")
print("• Grids work with viewport units (vw, vh) for responsive layouts")
print("• Use gridAutoFlow to control automatic item placement")
print("• gridAutoRows/gridAutoColumns define sizes for auto-generated tracks")
print("• Grids can be nested within grid items")
print("• Combine fixed (px) and flexible (fr) units for hybrid layouts")
print("• Use gaps to create visual separation between grid items")

View File

@@ -0,0 +1,426 @@
-- Grid Layout Tests
-- Tests for CSS Grid layout functionality
package.path = package.path .. ";?.lua"
local lu = require("testing/luaunit")
require("testing/loveStub") -- Required to mock LOVE functions
local FlexLove = require("FlexLove")
local Gui = FlexLove.GUI
local Color = FlexLove.Color
local enums = FlexLove.enums
TestGridLayout = {}
function TestGridLayout:setUp()
-- Reset GUI before each test
Gui.destroy()
Gui.init({})
end
function TestGridLayout:tearDown()
Gui.destroy()
end
-- ====================
-- Track Parsing Tests (via grid behavior)
-- ====================
function TestGridLayout:test_grid_accepts_various_track_formats()
-- Test that grid accepts various track size formats without errors
local grid1 = Gui.new({
x = 0,
y = 0,
width = 600,
height = 400,
positioning = enums.Positioning.GRID,
gridTemplateColumns = "100px 2fr 50%",
gridTemplateRows = "auto 1fr",
})
lu.assertNotNil(grid1)
local grid2 = Gui.new({
x = 0,
y = 0,
width = 600,
height = 400,
positioning = enums.Positioning.GRID,
gridTemplateColumns = "repeat(3, 1fr)",
gridTemplateRows = "repeat(2, 100px)",
})
lu.assertNotNil(grid2)
Gui.destroy()
end
-- ====================
-- Basic Grid Layout Tests
-- ====================
function TestGridLayout:test_simple_grid_creation()
local grid = Gui.new({
x = 0,
y = 0,
width = 600,
height = 400,
positioning = enums.Positioning.GRID,
gridTemplateColumns = "1fr 1fr 1fr",
gridTemplateRows = "1fr 1fr",
})
lu.assertEquals(grid.positioning, enums.Positioning.GRID)
lu.assertEquals(grid.gridTemplateColumns, "1fr 1fr 1fr")
lu.assertEquals(grid.gridTemplateRows, "1fr 1fr")
end
function TestGridLayout:test_grid_with_gaps()
local grid = Gui.new({
x = 0,
y = 0,
width = 600,
height = 400,
positioning = enums.Positioning.GRID,
gridTemplateColumns = "1fr 1fr",
gridTemplateRows = "1fr 1fr",
columnGap = 10,
rowGap = 20,
})
lu.assertEquals(grid.columnGap, 10)
lu.assertEquals(grid.rowGap, 20)
end
function TestGridLayout:test_grid_auto_placement()
local grid = Gui.new({
x = 0,
y = 0,
width = 300,
height = 200,
positioning = enums.Positioning.GRID,
gridTemplateColumns = "100px 100px 100px",
gridTemplateRows = "100px 100px",
columnGap = 0,
rowGap = 0,
padding = { horizontal = 0, vertical = 0 },
})
-- Add 6 items that should auto-place in a 3x2 grid
local items = {}
for i = 1, 6 do
items[i] = Gui.new({
parent = grid,
width = 50,
height = 50,
})
end
-- Check first item (top-left)
lu.assertAlmostEquals(items[1].x, 0, 1)
lu.assertAlmostEquals(items[1].y, 0, 1)
-- Check second item (top-middle)
lu.assertAlmostEquals(items[2].x, 100, 1)
lu.assertAlmostEquals(items[2].y, 0, 1)
-- Check fourth item (bottom-left)
lu.assertAlmostEquals(items[4].x, 0, 1)
lu.assertAlmostEquals(items[4].y, 100, 1)
end
function TestGridLayout:test_grid_explicit_placement()
local grid = Gui.new({
x = 0,
y = 0,
width = 300,
height = 200,
positioning = enums.Positioning.GRID,
gridTemplateColumns = "100px 100px 100px",
gridTemplateRows = "100px 100px",
columnGap = 0,
rowGap = 0,
padding = { horizontal = 0, vertical = 0 },
})
-- Place item at column 2, row 2
local item = Gui.new({
parent = grid,
gridColumn = 2,
gridRow = 2,
width = 50,
height = 50,
})
-- Should be at position (100, 100)
lu.assertAlmostEquals(item.x, 100, 1)
lu.assertAlmostEquals(item.y, 100, 1)
end
function TestGridLayout:test_grid_spanning()
local grid = Gui.new({
x = 0,
y = 0,
width = 300,
height = 200,
positioning = enums.Positioning.GRID,
gridTemplateColumns = "100px 100px 100px",
gridTemplateRows = "100px 100px",
columnGap = 0,
rowGap = 0,
padding = { horizontal = 0, vertical = 0 },
})
-- Item spanning columns 1-3
local item = Gui.new({
parent = grid,
gridColumn = "1 / 4",
gridRow = 1,
width = 50,
height = 50,
})
-- Should start at x=0 and span 300px (3 columns)
lu.assertAlmostEquals(item.x, 0, 1)
lu.assertAlmostEquals(item.width, 300, 1)
end
-- ====================
-- Track Sizing Tests
-- ====================
function TestGridLayout:test_fr_unit_distribution()
local grid = Gui.new({
x = 0,
y = 0,
width = 300,
height = 200,
positioning = enums.Positioning.GRID,
gridTemplateColumns = "1fr 2fr",
gridTemplateRows = "1fr",
columnGap = 0,
rowGap = 0,
padding = { horizontal = 0, vertical = 0 },
})
local item1 = Gui.new({
parent = grid,
gridColumn = 1,
gridRow = 1,
width = 50,
height = 50,
})
local item2 = Gui.new({
parent = grid,
gridColumn = 2,
gridRow = 1,
width = 50,
height = 50,
})
-- First column should be 100px (1fr), second should be 200px (2fr)
lu.assertAlmostEquals(item1.x, 0, 1)
lu.assertAlmostEquals(item2.x, 100, 1)
lu.assertAlmostEquals(item1.width, 100, 1)
lu.assertAlmostEquals(item2.width, 200, 1)
end
function TestGridLayout:test_mixed_units()
local grid = Gui.new({
x = 0,
y = 0,
width = 400,
height = 200,
positioning = enums.Positioning.GRID,
gridTemplateColumns = "100px 1fr 2fr",
gridTemplateRows = "1fr",
columnGap = 0,
rowGap = 0,
padding = { horizontal = 0, vertical = 0 },
})
local item1 = Gui.new({ parent = grid, gridColumn = 1, gridRow = 1, width = 50, height = 50 })
local item2 = Gui.new({ parent = grid, gridColumn = 2, gridRow = 1, width = 50, height = 50 })
local item3 = Gui.new({ parent = grid, gridColumn = 3, gridRow = 1, width = 50, height = 50 })
-- First column: 100px (fixed)
-- Remaining 300px divided as 1fr (100px) and 2fr (200px)
lu.assertAlmostEquals(item1.width, 100, 1)
lu.assertAlmostEquals(item2.width, 100, 1)
lu.assertAlmostEquals(item3.width, 200, 1)
end
function TestGridLayout:test_percentage_columns()
local grid = Gui.new({
x = 0,
y = 0,
width = 400,
height = 200,
positioning = enums.Positioning.GRID,
gridTemplateColumns = "25% 50% 25%",
gridTemplateRows = "1fr",
columnGap = 0,
rowGap = 0,
padding = { horizontal = 0, vertical = 0 },
})
local item1 = Gui.new({ parent = grid, gridColumn = 1, gridRow = 1, width = 50, height = 50 })
local item2 = Gui.new({ parent = grid, gridColumn = 2, gridRow = 1, width = 50, height = 50 })
local item3 = Gui.new({ parent = grid, gridColumn = 3, gridRow = 1, width = 50, height = 50 })
lu.assertAlmostEquals(item1.width, 100, 1) -- 25% of 400
lu.assertAlmostEquals(item2.width, 200, 1) -- 50% of 400
lu.assertAlmostEquals(item3.width, 100, 1) -- 25% of 400
end
-- ====================
-- Alignment Tests
-- ====================
function TestGridLayout:test_justify_items_stretch()
local grid = Gui.new({
x = 0,
y = 0,
width = 300,
height = 200,
positioning = enums.Positioning.GRID,
gridTemplateColumns = "100px 100px 100px",
gridTemplateRows = "100px",
justifyItems = enums.JustifyItems.STRETCH,
columnGap = 0,
rowGap = 0,
padding = { horizontal = 0, vertical = 0 },
})
local item = Gui.new({
parent = grid,
gridColumn = 1,
gridRow = 1,
height = 50,
})
-- Item should stretch to fill cell width
lu.assertAlmostEquals(item.width, 100, 1)
end
function TestGridLayout:test_align_items_stretch()
local grid = Gui.new({
x = 0,
y = 0,
width = 300,
height = 200,
positioning = enums.Positioning.GRID,
gridTemplateColumns = "100px",
gridTemplateRows = "100px 100px",
alignItems = enums.AlignItems.STRETCH,
columnGap = 0,
rowGap = 0,
padding = { horizontal = 0, vertical = 0 },
})
local item = Gui.new({
parent = grid,
gridColumn = 1,
gridRow = 1,
width = 50,
})
-- Item should stretch to fill cell height
lu.assertAlmostEquals(item.height, 100, 1)
end
-- ====================
-- Gap Tests
-- ====================
function TestGridLayout:test_column_gap()
local grid = Gui.new({
x = 0,
y = 0,
width = 320,
height = 100,
positioning = enums.Positioning.GRID,
gridTemplateColumns = "100px 100px 100px",
gridTemplateRows = "100px",
columnGap = 10,
rowGap = 0,
padding = { horizontal = 0, vertical = 0 },
})
local item1 = Gui.new({ parent = grid, gridColumn = 1, gridRow = 1, width = 50, height = 50 })
local item2 = Gui.new({ parent = grid, gridColumn = 2, gridRow = 1, width = 50, height = 50 })
local item3 = Gui.new({ parent = grid, gridColumn = 3, gridRow = 1, width = 50, height = 50 })
lu.assertAlmostEquals(item1.x, 0, 1)
lu.assertAlmostEquals(item2.x, 110, 1) -- 100 + 10 gap
lu.assertAlmostEquals(item3.x, 220, 1) -- 100 + 10 + 100 + 10
end
function TestGridLayout:test_row_gap()
local grid = Gui.new({
x = 0,
y = 0,
width = 100,
height = 320,
positioning = enums.Positioning.GRID,
gridTemplateColumns = "100px",
gridTemplateRows = "100px 100px 100px",
columnGap = 0,
rowGap = 10,
padding = { horizontal = 0, vertical = 0 },
})
local item1 = Gui.new({ parent = grid, gridColumn = 1, gridRow = 1, width = 50, height = 50 })
local item2 = Gui.new({ parent = grid, gridColumn = 1, gridRow = 2, width = 50, height = 50 })
local item3 = Gui.new({ parent = grid, gridColumn = 1, gridRow = 3, width = 50, height = 50 })
lu.assertAlmostEquals(item1.y, 0, 1)
lu.assertAlmostEquals(item2.y, 110, 1) -- 100 + 10 gap
lu.assertAlmostEquals(item3.y, 220, 1) -- 100 + 10 + 100 + 10
end
-- ====================
-- Nested Grid Tests
-- ====================
function TestGridLayout:test_nested_grids()
local outerGrid = Gui.new({
x = 0,
y = 0,
width = 400,
height = 400,
positioning = enums.Positioning.GRID,
gridTemplateColumns = "1fr 1fr",
gridTemplateRows = "1fr 1fr",
columnGap = 0,
rowGap = 0,
padding = { horizontal = 0, vertical = 0 },
})
local innerGrid = Gui.new({
parent = outerGrid,
gridColumn = 1,
gridRow = 1,
positioning = enums.Positioning.GRID,
gridTemplateColumns = "1fr 1fr",
gridTemplateRows = "1fr 1fr",
columnGap = 0,
rowGap = 0,
padding = { horizontal = 0, vertical = 0 },
})
local innerItem = Gui.new({
parent = innerGrid,
gridColumn = 2,
gridRow = 2,
width = 50,
height = 50,
})
-- Inner grid should be in top-left quadrant (200x200)
-- Inner item should be in bottom-right of that (at 100, 100 relative to inner grid)
lu.assertAlmostEquals(innerItem.x, 100, 1)
lu.assertAlmostEquals(innerItem.y, 100, 1)
end
print("Running Grid Layout Tests...")
os.exit(lu.LuaUnit.run())

View File

@@ -18,6 +18,7 @@ local testFiles = {
"testing/__tests__/12_units_system_tests.lua",
"testing/__tests__/13_relative_positioning_tests.lua",
"testing/__tests__/14_text_scaling_basic_tests.lua",
"testing/__tests__/15_grid_layout_tests.lua",
}
-- testingun all tests, but don't exit on error