Repository navigation
layout
Horizontal rows, multi-row cell layouts, vertical columns, fill-child regions, panels, and panel groups.
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)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 },
})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):
-
span = true- Span cell: single child window at full region height, callscontent() -
rowsis a non-empty array - Stack cell: each function rendered in its own nested child window -
contentis a function - Single content cell at full region height - 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:
-
defsnil or empty: returns immediately (silent) -
rowsnot a number or < 1: logs[WindowUtils] MultiRow '<id>': invalid rowsand returns - Cell has both
spanandrows:spanwins,rowsis ignored, no warning is logged
span is matched with == true, so a truthy non-boolean value (span = 1) will not enable span mode.
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,
} },
})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 |
An auto slot calls content() inline, directly in the parent, with no BeginChild around it. That has two consequences worth knowing:
-
bg,border, andflagsdo nothing on anautoslot. Wrap the content in aPanelif 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 },
})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.
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
|
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")
endThe 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)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 })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"
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 })