Repository navigation
expand
Automatic window resizing for toggle panels. When a panel opens, the window grows; when it closes, the window shrinks. Supports three sizing modes, drag-to-resize, and position anchoring. Panel sizes are grid-aligned to prevent drift on repeated open/close cycles.
Expand uses a two-level design:
-
Content level -
splitter.toggle()withexpand = truecallsexpand.init(),expand.cacheBase(), andexpand.afterRender()internally. These run inside child windows and only store state (never callSetWindowSize/SetWindowPos). -
Window level -
expand.applyWindowSize(windowName)must be called at the main window scope (insideBegin()/End(), outside any children) whereGetWindowSize()returns the actual window dimensions.
You typically interact with expand through splitter.toggle() opts and one call to expand.applyWindowSize(). Direct expand.* calls are only needed for auto mode measurement or programmatic control.
local wu = GetMod("WindowUtils")
local split = wu.Splitter
local expand = wu.Expand
-- Before Begin(): apply constraints with panel contribution
local basePct = split.getExpandConstraint("sidebar")
local panelPx = split.getExpandSizePx("sidebar")
if basePct then
local dw, dh = GetDisplayResolution()
local maxW = dw * basePct / 100 + panelPx
wu.SetConstraints(200, 200, maxW, dh * 0.8, "MyWindow")
end
ImGui.SetNextWindowPos(100, 100, ImGuiCond.FirstUseEver)
if ImGui.Begin("MyWindow") then
split.toggle("sidebar", {
{ content = drawSidebar },
{ content = drawMain },
}, {
side = "right",
size = 250,
expand = true,
windowName = "MyWindow",
sizeMode = "fixed",
normalConstraintPct = 25,
})
-- Window-level call: drives SetWindowSize/SetWindowPos
expand.applyWindowSize("MyWindow")
end
ImGui.End()The window grows/shrinks when the panel opens/closes or is dragged. The panel stays at its configured size. Dragging the bar adjusts the window width/height.
split.toggle("panel", panels, {
expand = true,
windowName = "MyWindow",
sizeMode = "fixed", -- default
size = 250,
})Drag behavior: Window resizes. Panel size stays constant.
The window stays the same size. The panel takes space from the flex (main) panel. Dragging the bar redistributes space between the fixed and flex panels.
split.toggle("panel", panels, {
expand = true,
windowName = "MyWindow",
sizeMode = "flex",
size = 250,
})Drag behavior: Panel resizes. Window stays constant. Manual window resize maintains the panel/window ratio.
The panel size is driven by its content (measured each frame via expand.setMeasuredSize()). The window grows/shrinks to fit. Dragging the bar adds/removes extra space around the content.
split.toggle("panel", panels, {
expand = true,
windowName = "MyWindow",
sizeMode = "auto",
size = 200, -- initial size before first measurement
})
-- Inside the panel content callback, measure and report:
local contentHeight = ImGui.GetCursorPosY()
expand.setMeasuredSize("panel", contentHeight)Drag behavior: Window resizes (same as fixed). Panel stays at measured content size.
These opts are passed to splitter.toggle() to enable expand mode:
| Option | Type | Default | Description |
|---|---|---|---|
| expand | boolean | false | Enable expand mode |
| windowName | string | - | ImGui window name (must match Begin()) |
| sizeMode | string | "fixed" |
"fixed", "flex", or "auto"
|
| size | number|string | 200 | Panel size in pixels or percentage |
| side | string | "left" |
"left", "right", "top", "bottom"
|
| normalConstraintPct | number|nil | nil | Base max-size constraint as display % (without panel) |
windowName is mandatory here, because expand indexes its panels by window name. splitter.toggle with expand = true and no windowName logs expand.init: '<id>' requires opts.windowName and skips registration, so expand mode silently does nothing.
Of these, sizeMode, size, and side are re-read every frame by splitter.toggle. windowName and normalConstraintPct are captured when the panel state is created and ignored afterwards. See toggle for the full option lifetime table.
When normalConstraintPct is set, the expand system provides two values for building window constraints:
-
splitter.getExpandConstraint(id)- returnsnormalConstraintPct(the base max without panel) -
splitter.getExpandSizePx(id)- returns the panel's current pixel contribution (0 when closed, animated during toggle, grid-aligned when settled)
Add the panel contribution to both min and max constraints:
local basePct = split.getExpandConstraint("panel")
local panelPx = split.getExpandSizePx("panel")
if basePct then
local dw, dh = GetDisplayResolution()
local maxH = dh * basePct / 100 + panelPx
local minH = 200 + panelPx
wu.SetConstraints(200, minH, dw * 0.5, maxH, "MyWindow")
endThe smooth transition during open/close comes from getExpandSizePx returning the toggle's animated panel size. No separate constraint animation is needed.
These functions are exposed on wu.Expand. Most are called internally by splitter.toggle() - you only need them for auto mode or programmatic control.
These run inside child windows and only store state. Called internally by splitter.toggle().
Register or update an expand panel configuration. Idempotent - safe to call every frame. On first call, creates the panel state; on subsequent calls, only size and sizeMode are refreshed. windowName, side, and normalConstraintPct are captured once and later values are ignored. Switching sizeMode clears the mode-specific drag/measure state and forces one settling resize.
windowName must be a non-nil string, since init files the panel under its window in an internal index. A missing name logs expand.init: '<id>' requires opts.windowName and returns early, leaving the panel unregistered.
| Parameter | Type | Description |
|---|---|---|
| id | string | Panel identifier (same id used with splitter.toggle) |
| opts | table | Configuration table (see below) |
opts fields:
| Field | Type | Default | Description |
|---|---|---|---|
| windowName | string | - | ImGui window name (must match Begin()) |
| side | string | "right" |
"left", "right", "top", "bottom"
|
| size | number | 200 | Panel size in pixels |
| sizeMode | string | "fixed" |
"fixed", "flex", or "auto"
|
| normalConstraintPct | number|nil | nil | Base max-size constraint as display % |
Signal a toggle event. Marks the panel as unsettled so applyWindowSize will resize the window on the next frame.
| Parameter | Type | Description |
|---|---|---|
| id | string | Panel identifier |
| isOpen | boolean | New open state (after the toggle) |
Cache the content-region available space. Only updates baseAvail when the panel has been closed for 2+ frames, ensuring SetWindowSize lag has resolved.
| Parameter | Type | Description |
|---|---|---|
| id | string | Panel identifier |
| totalAvail | number | Current content region available (from GetContentRegionAvail) |
| panelSize | number | Current animated panel size (0 when closed) |
Get the cached base content-region available space. This is the "naked" content width/height before any expand panels are added.
Returns: number|nil
Get the effective panel target size, grid-aligned. In auto mode this is the committed measured size, which lags setMeasuredSize by one frame to avoid jitter, falling back to the raw measurement before anything has been committed. In fixed and flex modes it is dragSize when a drag size exists (flex drag, or a size restored from persistence), otherwise the configured panelSizePx. The returned value is rounded up to the next grid unit boundary when grid snapping is enabled.
Returns: number|nil - nil when the id is unknown, or in auto mode before the first measurement
Report measured content size for auto mode. Call this inside the panel's content callback after rendering.
| Parameter | Type | Description |
|---|---|---|
| id | string | Panel identifier |
| size | number | Measured content extent in pixels |
Apply drag delta to an expand panel. In fixed mode, adjusts the window via dragOffset. In flex mode, adjusts the panel size via dragSize.
| Parameter | Type | Description |
|---|---|---|
| id | string | Panel identifier |
| delta | number | Cumulative drag delta in pixels (from GetMouseDragDelta) |
| dirMul | number | Direction multiplier: +1 for right/bottom, -1 for left/top |
Finalize drag state. In fixed mode, commits the drag offset to the window base dimensions. In flex mode, clears the drag start reference and lets the ratio reconciliation phase handle it.
| Parameter | Type | Description |
|---|---|---|
| id | string | Panel identifier |
Store per-frame panel state for applyWindowSize() to consume. Called after the panel content has been rendered.
| Parameter | Type | Description |
|---|---|---|
| id | string | Panel identifier |
| panelSize | number | Current animated panel size |
| isAnimating | boolean | Whether the splitter animation is in progress |
| isDragging | boolean | Whether the user is currently dragging the expand bar |
Must be called at main window scope (inside Begin()/End(), outside any children).
Drive window resizing for all expand panels on a window. Must be called at window scope (inside Begin()/End(), outside any child windows).
Handles: panel contribution summing, base dimension tracking, resize determination, manual resize detection, and position anchoring for left/top panels.
Get the base max constraint percentage (without panel contribution). Returns normalConstraintPct as configured in init, or nil if not configured.
| Parameter | Type | Description |
|---|---|---|
| id | string | Panel identifier |
Returns: number|nil - base max constraint as display %
Get the current panel size contribution in pixels for constraint adjustment. Returns the live panel size while dragging or animating, the grid-aligned panel size when open and settled, and 0 when closed or when the id is unknown.
| Parameter | Type | Description |
|---|---|---|
| id | string | Panel identifier |
| isOpen | boolean | Current open state |
Returns: number - pixels to add to min/max constraints
Mark an expand panel as needing a window resize for the given state. This is the same code path as onToggle: it does not open or close anything on its own, because the open state lives in the splitter toggle, not in expand.
To actually open or close a panel programmatically, call splitter.setToggle(id, open), which flips the toggle and notifies expand for you.
| Parameter | Type | Description |
|---|---|---|
| id | string | Panel identifier |
| isOpen | boolean | State the window should be resized for |
Remove all internal state for an expand panel. Also removes the panel from its window's panel index. If the last panel on a window is destroyed, the window's base dimensions are also cleaned up.
| Parameter | Type | Description |
|---|---|---|
| id | string | Panel identifier |
Double-clicking the toggle bar opens/closes the panel. Works in all three size modes. Pass toggleOnClick = true to splitter.toggle for single-click toggling instead; drag still works, and a drag cancels the pending toggle.
Dragging the toggle bar resizes the panel, but only while the panel is open and its animation has finished. On a closed or still-animating panel the bar shows the hand cursor and drags are ignored.
- Fixed/Auto: Window grows or shrinks. Bar shows resize cursor.
- Flex: Panel redistributes space within the window. Bar shows resize cursor.
Drag offsets persist across toggle cycles - closing and reopening the panel keeps any size adjustment.
For left and top side panels, the window's right/bottom edge stays anchored during animation and drag. This prevents the content area from visually jumping.
local wu = GetMod("WindowUtils")
local split = wu.Splitter
local expand = wu.Expand
-- Pre-Begin constraint
local basePct = split.getExpandConstraint("settings")
local panelPx = split.getExpandSizePx("settings")
if basePct then
local dw, dh = GetDisplayResolution()
local maxW = dw * basePct / 100 + panelPx
wu.SetConstraints(200, 200, maxW, dh * 0.8, "MyMod")
end
if ImGui.Begin("MyMod") then
split.toggle("settings", {
{ content = function()
ImGui.Text("Settings go here")
end },
{ content = function()
ImGui.Text("Main content")
end },
}, {
side = "right",
size = 280,
expand = true,
windowName = "MyMod",
sizeMode = "fixed",
normalConstraintPct = 25,
})
expand.applyWindowSize("MyMod")
end
ImGui.End()split.toggle("inspector", {
{ content = drawInspector },
{ content = drawViewport },
}, {
side = "right",
size = 300,
expand = true,
windowName = "Editor",
sizeMode = "flex",
})
expand.applyWindowSize("Editor")split.toggle("details", {
{ content = function()
-- Render variable-height content
for _, item in ipairs(items) do
ImGui.Text(item.name)
end
-- Report measured size
expand.setMeasuredSize("details", ImGui.GetCursorPosY())
end },
{ content = drawMain },
}, {
side = "bottom",
size = 100, -- initial estimate
expand = true,
windowName = "MyApp",
sizeMode = "auto",
})
expand.applyWindowSize("MyApp")-- Left sidebar + bottom details - both expand the same window
split.toggle("sidebar", {
{ content = drawSidebar },
{ content = function()
split.toggle("details", {
{ content = drawDetails },
{ content = drawMain },
}, {
side = "bottom",
size = 150,
expand = true,
windowName = "App",
sizeMode = "fixed",
})
end },
}, {
side = "left",
size = 220,
expand = true,
windowName = "App",
sizeMode = "fixed",
})
-- Single call handles both panels
expand.applyWindowSize("App")split.toggle("bottomPanel", {
{ content = drawConsole },
{ content = drawEditor },
}, {
side = "bottom",
size = 200,
expand = true,
windowName = "IDE",
sizeMode = "fixed",
})
expand.applyWindowSize("IDE")