From da3581785fda3f241af5d1d061908310534ca17d Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Fri, 10 Oct 2025 22:18:30 -0400 Subject: [PATCH] start grid impl --- FlexLove.lua | 538 ++++++++++++++++++++- examples/BasicGrid.lua | 226 +++++++++ examples/ResponsiveGrid.lua | 249 ++++++++++ testing/__tests__/15_grid_layout_tests.lua | 426 ++++++++++++++++ testing/runAll.lua | 1 + 5 files changed, 1434 insertions(+), 6 deletions(-) create mode 100644 examples/BasicGrid.lua create mode 100644 examples/ResponsiveGrid.lua create mode 100644 testing/__tests__/15_grid_layout_tests.lua diff --git a/FlexLove.lua b/FlexLove.lua index 6a2509f..02ea9ed 100644 --- a/FlexLove.lua +++ b/FlexLove.lua @@ -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 @@ -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 diff --git a/examples/BasicGrid.lua b/examples/BasicGrid.lua new file mode 100644 index 0000000..a3ba9b1 --- /dev/null +++ b/examples/BasicGrid.lua @@ -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") diff --git a/examples/ResponsiveGrid.lua b/examples/ResponsiveGrid.lua new file mode 100644 index 0000000..ca9d565 --- /dev/null +++ b/examples/ResponsiveGrid.lua @@ -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") diff --git a/testing/__tests__/15_grid_layout_tests.lua b/testing/__tests__/15_grid_layout_tests.lua new file mode 100644 index 0000000..2d7c857 --- /dev/null +++ b/testing/__tests__/15_grid_layout_tests.lua @@ -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()) diff --git a/testing/runAll.lua b/testing/runAll.lua index 0aa97af..5868794 100644 --- a/testing/runAll.lua +++ b/testing/runAll.lua @@ -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