Skip to content
CyanideX edited this page Aug 5, 2026 · 1 revision

Layout Controls

Horizontal rows, multi-row cell layouts, vertical columns, fill-child regions, panels, and panel groups.

Quick Start

local wu = GetMod("WindowUtils")
local c = wu.Controls

-- Horizontal row: fixed sidebar + flex content
c.Row("layout", {
    { width = 200, content = drawSidebar },
    { content = drawMainContent },
})

-- Fill remaining vertical space with scrollable content
c.BeginFillChild("scrollArea", { footerHeight = 30 })
-- scrollable content here
c.EndFillChild("scrollArea")

-- Styled panel
c.Panel("info", function()
    ImGui.Text("Panel content")
end)

Row(id, defs, opts?)

Horizontal row of child windows that fills available width. Uses tight horizontal spacing by default.

Parameter Type Default Description
id string - Unique row ID
defs table - Array of child definitions
opts.height number|nil nil Row height (nil = auto)
opts.gap number|nil ItemSpacing.y Gap between children (tight default)
opts.normalSpacing boolean false Use standard ItemSpacing.x instead of tight spacing

Child definition fields:

Field Type Default Description
content function - Renders child content
width number|nil nil Fixed width in pixels
cols number|nil nil Width from ColWidth grid (1-12)
flex number|nil 1 Proportional share of remaining space
border boolean false Show child border
flags number 0 Extra ImGui window flags
bg table|nil nil Background color {r,g,b,a}
c.Row("layout", {
    { width = 200, content = drawSidebar },
    { content = drawMainContent },
})

MultiRow(id, rows, defs, opts?)

Horizontal row of child windows where cells can span the full vertical height or stack multiple rows of normal-height controls. Useful for placing a tall button alongside stacked controls. Uses tight horizontal spacing by default.

Parameter Type Default Description
id string - Unique row ID prefix
rows number - Number of visual rows (determines region height)
defs table - Array of cell definitions
opts.gap number|nil ItemSpacing.y Horizontal spacing between cells (tight default)
opts.normalSpacing boolean false Use standard ItemSpacing.x instead of tight spacing

Height calculation:

Region height = rows * (frameCache.frameHeight + frameCache.itemSpacingY)

Cell definition fields:

Field Type Default Description
width number|nil nil Fixed width in pixels
cols number|nil nil Width from ColWidth grid (1-12)
flex number|nil 1 Proportional share of remaining space
span boolean|nil nil If true, single child at full region height
rows table|nil nil Array of content functions for stacked rows
content function|nil nil Single content function
bg table|nil nil Background color {r,g,b,a}
border boolean false Show child border
flags number 0 Extra ImGui window flags

Cell type resolution (first match wins):

  1. span = true - Span cell: single child window at full region height, calls content()
  2. rows is a non-empty array - Stack cell: each function rendered in its own nested child window
  3. content is a function - Single content cell at full region height
  4. None of the above - Empty child window

Stack row height = floor((regionHeight - (N - 1) * itemSpacingY) / N) where N is the number of stacked rows.

Error handling:

  • defs nil or empty: returns immediately (silent)
  • rows not a number or < 1: logs [WindowUtils] MultiRow '<id>': invalid rows and returns
  • Cell has both span and rows: span wins, rows is ignored, no warning is logged

span is matched with == true, so a truthy non-boolean value (span = 1) will not enable span mode.

Examples

Span cell with tall button:

c.MultiRow("demo", 2, {
    { cols = 3, span = true, content = function()
        local _, h = ImGui.GetContentRegionAvail()
        c.Button("Tall", "active", -1, h)
    end },
    { rows = {
        function() c.Button("Top", "inactive", -1) end,
        function() c.Button("Bottom", "inactive", -1) end,
    } },
})

Stack cell with three rows:

c.MultiRow("stack", 3, {
    { rows = {
        function() c.Button("Row 1", "inactive", -1) end,
        function() c.Button("Row 2", "inactive", -1) end,
        function() c.Button("Row 3", "inactive", -1) end,
    } },
})

Mixed layout with flex widths:

c.MultiRow("mixed", 2, {
    { flex = 1, span = true, content = function()
        local _, h = ImGui.GetContentRegionAvail()
        c.Button("Span", "active", -1, h)
    end },
    { flex = 2, rows = {
        function() c.Button("A", "inactive", -1) end,
        function() c.Button("B", "inactive", -1) end,
    } },
    { flex = 1, content = function()
        c.Button("Single", "inactive", -1)
    end },
})

Background and border:

c.MultiRow("styled", 2, {
    { bg = { 0.2, 0.4, 0.8, 0.15 }, border = true, span = true, content = function()
        local _, h = ImGui.GetContentRegionAvail()
        c.Button("Highlighted", "active", -1, h)
    end },
    { rows = {
        function() c.Button("Normal 1", "inactive", -1) end,
        function() c.Button("Normal 2", "inactive", -1) end,
    } },
})

Grid layout inside stack rows (Row/ColWidth for per-row column spans):

c.MultiRow("grid", 3, {
    { rows = {
        function()
            c.Row("r1", {
                { cols = 3, content = function() c.Button("1", "inactive", -1) end },
                { cols = 6, content = function() c.Button("2-wide", "active", -1) end },
                { cols = 3, content = function() c.Button("1", "inactive", -1) end },
            })
        end,
        function()
            c.Row("r2", {
                { content = function() c.Button("Left", "inactive", -1) end },
                { content = function() c.Button("Right", "active", -1) end },
            })
        end,
        function()
            c.SliderInt("TuneVariant", "slider", val, 0, 100)
        end,
    } },
})

Column(id, defs, opts?)

Vertical column of child windows that fills available height. Children can be flex (proportional), fixed height, or auto-sized.

Parameter Type Default Description
id string - Unique column ID
defs table - Array of child definitions
opts.gap number|nil ItemSpacing.y x 2 Gap between children (0 = flush)

Column has no normalSpacing option; it manages vertical cursor position directly instead of pushing ItemSpacing.

Child definition fields:

Field Type Default Description
content function - Renders child content
flex number|nil 1 Proportional share of remaining height
height number|nil nil Fixed height (auto-adds NoScrollbar)
auto boolean|nil nil Auto-size to content (no child window)
border boolean false Show child border. Ignored on auto slots
flags number 0 Extra ImGui window flags. Ignored on auto slots
bg table|nil nil Background color {r,g,b,a}. Ignored on auto slots

auto slots

An auto slot calls content() inline, directly in the parent, with no BeginChild around it. That has two consequences worth knowing:

  • bg, border, and flags do nothing on an auto slot. Wrap the content in a Panel if you need a background or border there.
  • Height is measured from the cursor delta on the previous frame, so on the first frame the slot reports 0 height and the gap after it is suppressed. It settles on the second frame.

Auto heights are cached per column id and invalidated when the number of defs changes, so keep the def count stable across frames where you can.

c.Column("page", {
    { flex = 1, content = drawTopPanel },
    { flex = 1, content = drawBottomPanel },
    { auto = true, content = function()
        if c.Button("Reset", "inactive") then reset() end
    end },
})

BeginFillChild(id, opts?)

Child region that fills remaining vertical space.

Parameter Type Default Description
id string - Child window ID (## prefix added automatically if missing)
opts.footerHeight number 0 Reserve space at bottom. When non-zero, one ItemSpacing.y is also reserved
opts.border boolean false Show border
opts.flags number 0 Extra ImGui window flags (AlwaysUseWindowPadding is always applied)
opts.bg table|false|nil subtle blue Background color {r,g,b,a}. Omit for the standard panel background, pass false for transparent
opts.elementId string|nil nil Tutorial/bounds target ID, reported by EndFillChild while bounds reporting is active

Returns: boolean - true if the child region is visible (not clipped or collapsed)

Child height is contentAvail - footerHeight - spacing, floored at 1px.

EndFillChild(id)

End fill child. Pass the same id used in BeginFillChild so the background color, scrollbar style, and bounds report resolve against the right entry.

Parameter Type Description
id string Child ID passed to BeginFillChild

Always call EndFillChild

BeginFillChild returns a visibility boolean, not a "did it open" boolean. The ImGui child, and the scrollbar and background styles around it, are pushed regardless of what it returns. So EndFillChild must be called unconditionally, on every path, or the ImGui stack is left unbalanced and the window will misrender or crash.

-- Correct: End is not conditional
c.BeginFillChild("scrollArea", { footerHeight = 30 })
-- content
c.EndFillChild("scrollArea")

-- Wrong: skipping End when the region is clipped corrupts the ImGui stack
if c.BeginFillChild("scrollArea") then
    -- content
    c.EndFillChild("scrollArea")
end

The return value is worth reading only as a hint that content is offscreen, so you can skip expensive work inside the region. Skip the content, never the End:

local visible = c.BeginFillChild("scrollArea", { footerHeight = 30 })
if visible then
    drawExpensiveList()
end
c.EndFillChild("scrollArea")

Calling EndFillChild(nil) still ends the child and pops the scrollbar style, and logs [WindowUtils] EndFillChild: nil id, but it cannot pop the background color. Always pass the ID.

c.BeginFillChild("scrollArea", { footerHeight = 30, bg = {0.1, 0.1, 0.1, 0.5} })
-- scrollable content
c.EndFillChild("scrollArea")
ImGui.Button("Footer Button", c.RemainingWidth(), 0)

Panel(id, contentFn, opts?)

Child region with optional background color and border. Always has internal padding (AlwaysUseWindowPadding). A simpler alternative to BeginFillChild/EndFillChild for non-expanding regions.

Parameter Type Default Description
id string - Child window ID (auto-prefixed with ## if needed)
contentFn function - Renders panel content
opts.bg table|false subtle blue Background color {r,g,b,a} or false for none
opts.border boolean false Show border
opts.borderOnHover boolean false Show border only when hovered
opts.width number 0 Panel width (0 = fill available)
opts.height number|"auto" 0 Panel height (0 = fill remaining height, "auto" = measured from content on the previous frame)
opts.flags number 0 Extra ImGui window flags (AlwaysUseWindowPadding is always applied)
opts.resizable boolean false Add a drag handle at the bottom edge for vertical resizing
opts.minHeight number 50 Minimum height when resizable
opts.maxHeight number 2000 Maximum height when resizable
opts.elementId string|nil nil Tutorial/bounds target ID. Only reported while bounds reporting is active

resizable and height = "auto" are mutually exclusive. When resizable is set, auto measurement is skipped and the height comes from the drag state instead, seeded from opts.height when it is a positive number and 200 otherwise. The dragged height persists per panel id for the session; it is not saved to disk.

On the first frame a height = "auto" panel falls back to 100px before its measurement lands.

c.Panel("info", function()
    ImGui.Text("Panel content")
end)

c.Panel("interactive", function()
    ImGui.Text("Hover me")
end, { borderOnHover = true })

c.Panel("fixed", drawContent, { width = 300, height = 200 })

getPanelAutoHeight(id)

Read the content height a height = "auto" panel measured on the previous frame.

Parameter Type Description
id string Panel ID (without the ## prefix, matching what you passed to Panel)

Returns: number|nil - measured height in pixels, or nil if that panel has not drawn yet or does not use height = "auto"

PanelGroup(id, contentFn, opts?)

Visual panel wrapper that constrains content width with symmetric padding. Uses a BeginChild sub-region internally so that GetContentRegionAvail() correctly reports the reduced width for all child elements (buttons, rows, etc.). Background and border are drawn via DrawList with rounding.

Use this instead of Panel when you need a styled panel that participates in the parent scroll region without creating a nested scroll context. Content height auto-sizes to fit.

Parameter Type Default Description
id string - Unique identifier (for hover state tracking)
contentFn function|nil nil Callback that renders panel content
opts.bg table|false subtle blue Background color {r,g,b,a} or false for none
opts.border boolean false Show border. Matched with == true, so a truthy non-boolean will not enable it
opts.borderOnHover boolean false Show border only when hovered
opts.elementId string|nil nil Tutorial/bounds target ID. Only reported while bounds reporting is active

borderOnHover does hit-testing against the panel rect using the raw mouse position, then tracks hover and draws the border.

The inner child window is forced to NoScrollbar + NoScrollWithMouse and sized from the previous frame's measured content height, so the first frame renders at 0 height. Content taller than the parent will be clipped rather than scrolled; use Panel when the region needs its own scrollbar.

c.PanelGroup("info", function()
    ImGui.Text("This panel scrolls with its parent")
    ImGui.Text("No nested scroll context")
end)

c.PanelGroup("hover_panel", function()
    ImGui.Text("Hover to see border")
end, { borderOnHover = true })

Clone this wiki locally