Skip to content
CyanideX edited this page Aug 5, 2026 · 2 revisions

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.

Architecture

Expand uses a two-level design:

  1. Content level - splitter.toggle() with expand = true calls expand.init(), expand.cacheBase(), and expand.afterRender() internally. These run inside child windows and only store state (never call SetWindowSize/SetWindowPos).

  2. Window level - expand.applyWindowSize(windowName) must be called at the main window scope (inside Begin()/End(), outside any children) where GetWindowSize() 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.

Quick Start

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()

Size Modes

Fixed (default)

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.

Flex

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.

Auto

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.

Toggle Options (expand mode)

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.

Constraint Sizing

When normalConstraintPct is set, the expand system provides two values for building window constraints:

  • splitter.getExpandConstraint(id) - returns normalConstraintPct (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")
end

The smooth transition during open/close comes from getExpandSizePx returning the toggle's animated panel size. No separate constraint animation is needed.

Expand API

These functions are exposed on wu.Expand. Most are called internally by splitter.toggle() - you only need them for auto mode or programmatic control.

Content-Level Functions

These run inside child windows and only store state. Called internally by splitter.toggle().

init(id, opts)

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 %

onToggle(id, isOpen)

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)

cacheBase(id, totalAvail, panelSize)

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)

getBaseAvail(id)

Get the cached base content-region available space. This is the "naked" content width/height before any expand panels are added.

Returns: number|nil

getTargetSize(id)

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

setMeasuredSize(id, size)

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

applyDrag(id, delta, dirMul)

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

commitDrag(id)

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

afterRender(id, panelSize, isAnimating, isDragging)

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

Window-Level Functions

Must be called at main window scope (inside Begin()/End(), outside any children).

applyWindowSize(windowName)

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.

Constraint and Control Functions

getConstraint(id)

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 %

getConstraintSizePx(id, isOpen)

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

setOpen(id, isOpen)

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

destroy(id)

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

Interaction

Double-Click Toggle

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.

Drag to Resize

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.

Position Anchoring

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.

Examples

Fixed Mode - Settings Sidebar

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()

Flex Mode - Resizable Inspector

split.toggle("inspector", {
    { content = drawInspector },
    { content = drawViewport },
}, {
    side = "right",
    size = 300,
    expand = true,
    windowName = "Editor",
    sizeMode = "flex",
})

expand.applyWindowSize("Editor")

Auto Mode - Dynamic Content Panel

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")

Multiple Expand Panels on One Window

-- 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")

Vertical Expand

split.toggle("bottomPanel", {
    { content = drawConsole },
    { content = drawEditor },
}, {
    side = "bottom",
    size = 200,
    expand = true,
    windowName = "IDE",
    sizeMode = "fixed",
})

expand.applyWindowSize("IDE")

Clone this wiki locally