Repository navigation
popout
Detachable panels that can dock inline or float in a separate window. Two visual styles: "panel" (default panel background) and "inline" (no background, blends with parent).
The module never auto-renders toggle buttons. Use toggleButton() inside your content callback or anywhere else in your UI to let users dock/undock.
Floating windows for detached popouts are rendered centrally by WindowUtils each frame, not at the call site. This means detached popouts persist even when their popout() call is inactive (e.g., the tab was switched away). No special handling is needed by the consumer.
local wu = GetMod("WindowUtils")
local pop = wu.Popout
-- Panel-style: styled background, placeholder with dock button when detached
pop.popout("my_panel", {
title = "My Panel",
style = "panel",
content = function()
ImGui.Text("Panel content")
wu.Controls.ButtonRow({ pop.toggleButton("my_panel") })
end,
})
-- Inline-style: no background, no auto buttons
pop.popout("my_inline", {
title = "Transport",
style = "inline",
content = function()
wu.Controls.ButtonRow({ pop.toggleButton("my_inline") })
ImGui.Text("Inline content")
end,
})Main rendering function. Call once per frame where the docked content should appear. When detached, the floating window is rendered automatically by WindowUtils at the top level (via drawAll()), so it persists even if this call site becomes inactive.
| Parameter | Type | Default | Description |
|---|---|---|---|
| id | string | - | Unique popout identifier |
| opts.content | function | nil | Callback that renders the panel content |
| opts.title | string | id | Title text shown centered in the floating window |
| opts.style | string | "panel" |
"panel" or "inline"
|
| opts.defaultDocked | boolean | true | Initial dock state on first use |
| opts.size | table | {width=300, height=200} | Floating window size {width, height} in pixels |
| opts.widthPercent | number | nil | Width as display percentage (overrides size.width, also sets min width) |
| opts.fitHeight | boolean | false | Auto-size height to content, snapped to next grid cell |
| opts.minSize | table|nil | nil | Minimum floating window size {width, height}
|
| opts.maxSize | table|nil | nil | Maximum floating window size {width, height}
|
| opts.flags | number|nil | nil | Extra ImGui window flags for the floating window |
| opts.icon | string|nil | nil | Glyph for the placeholder dock button (panel style only) |
| opts.bg | table|false|nil | nil | Background: nil = style default, false = none, table = custom RGBA |
| opts.placeholder | function|nil | nil | Content rendered in the placeholder when detached |
| opts.hideWhenDetached | boolean | false | Skip placeholder entirely when detached |
| opts.showTitle | boolean | true | Show centered title in the floating window |
| opts.sideHandle | boolean | false | Show vertical grab handle on the left side of the floating window |
Returns: boolean - current docked state (true = docked, false = detached). Same value as isDocked(id), convenient when you are already calling popout().
Option lifetime: style, title, defaultDocked, size, and icon are read only on the first call for a given id and are ignored afterwards. Everything else (content, bg, placeholder, widthPercent, fitHeight, minSize, maxSize, flags, showTitle, hideWhenDetached, sideHandle) is re-read every frame. An invalid style falls back to "panel".
The floating window is drawn from the opts captured on the most recent popout() call, so a detached popout whose call site stops running keeps rendering with its last known opts.
Background behavior:
-
"panel"style: default panel background in docked, placeholder, and floating states -
"inline"style: no background in any state -
bg = falsedisables background on any style;bg = {r, g, b, a}enables a custom one
Placeholder behavior when detached:
-
"panel"style: styled panel with a dock button and optionalplaceholdercallback (SameLine) -
"inline"style: onlyplaceholdercallback if provided, no auto button -
hideWhenDetached = true: skips placeholder entirely
fitHeight behavior:
- Auto-sizes the floating window height to content, ceiled to the next grid cell boundary
- Width is user-resizable (drag to resize);
widthPercentsets the initial and minimum width - Uses a two-phase approach: measures content for 1-2 frames, then locks the Panel to fill the grid-ceiled window height
- Re-measures automatically if the user resizes width (content may reflow)
Flip the dock state. No-op if ID hasn't been rendered yet.
Renders floating windows for all detached popouts. Called automatically by WindowUtils once per frame at the top level. Consumers do not need to call this.
Set the dock state programmatically. No-op if the ID hasn't been rendered yet. Dock state is not persisted across sessions; each session starts at defaultDocked.
Query the current dock state.
Returns: boolean - true when docked, false when detached, and true for an unknown id (matching the defaultDocked default).
Returns IconGlyphs.OpenInNew when docked or when the id is unknown, IconGlyphs.DockWindow when detached.
Returns a controls.ButtonRow-compatible button definition wired to toggle(id). The icon updates with the dock state.
The opts parameter is accepted but not used.
Returns: table - { type = "button", icon, tooltip, onClick }
The tooltip is derived from isDocked(), so it flips with the dock state alongside the icon.
Remove internal state for a popout ID. Call when dynamically created popouts are no longer needed.
pop.popout("settings", {
title = "Settings",
style = "panel",
content = function()
ImGui.Text("Settings content")
controls.ButtonRow({ pop.toggleButton("settings") })
end,
placeholder = function()
controls.TextMuted("Settings panel is floating.")
end,
})pop.popout("tools", {
title = "Tools",
style = "panel",
hideWhenDetached = true,
content = function()
ImGui.Text("Tool content")
controls.ButtonRow({ pop.toggleButton("tools") })
end,
})Auto-sizes height to content (grid-snapped). Width starts at 25% of display and is user-resizable (25% is also the minimum).
pop.popout("compact", {
title = "Compact Panel",
style = "panel",
widthPercent = 25,
fitHeight = true,
content = function()
ImGui.Text("Height fits content, snapped to grid.")
controls.ButtonRow({ pop.toggleButton("compact") })
end,
})pop.popout("constrained", {
title = "Constrained",
style = "panel",
size = { width = 300, height = 200 },
minSize = { width = 200, height = 150 },
maxSize = { width = 500, height = 400 },
content = function()
ImGui.Text("Resizable within constraints")
controls.ButtonRow({ pop.toggleButton("constrained") })
end,
})pop.popout("transport", {
title = "Transport",
style = "inline",
content = function()
controls.ButtonRow({ pop.toggleButton("transport") })
ImGui.Text("Transport controls")
end,
})pop.popout("transport", {
title = "Transport",
style = "inline",
bg = { 0.65, 0.7, 1.0, 0.045 },
content = function()
controls.ButtonRow({ pop.toggleButton("transport") })
ImGui.Text("Transport controls with background")
end,
})pop.popout("raw", {
title = "Raw Panel",
style = "panel",
bg = false,
content = function()
ImGui.Text("No background")
controls.ButtonRow({ pop.toggleButton("raw") })
end,
})-- Button in a toolbar that controls a popout defined elsewhere
controls.ButtonRow({
pop.toggleButton("detail_panel"),
{ label = " Save ", style = "active", onClick = save },
})
-- The popout itself, rendered in a different part of the layout
pop.popout("detail_panel", {
title = "Details",
style = "panel",
content = function()
ImGui.Text("Detail content")
end,
})split.toggle("sidebar", {
{ content = function()
pop.popout("props", {
title = "Properties",
style = "panel",
content = function()
ImGui.Text("Property editor")
controls.ButtonRow({ pop.toggleButton("props") })
end,
})
end },
{ content = drawMain },
}, { side = "right", size = 280 })pop.setDocked("my_panel", false)
local isDocked = pop.popout("my_panel", {
title = "My Panel",
content = function()
ImGui.Text("Panel content")
end,
})
-- pop.isDocked("my_panel") reports the same state anywhere else in your UI
if isDocked then
ImGui.Text("Panel is docked")
end
if wu.Controls.Button(" Toggle ") then
pop.toggle("my_panel")
endpop.popout("compact", {
title = "Compact",
style = "panel",
showTitle = false,
content = function()
ImGui.Text("No title, content starts immediately.")
controls.ButtonRow({ pop.toggleButton("compact") })
end,
})Vertical grip bar on the left, content in a child window to the right.
pop.popout("tools", {
title = "Tools",
style = "panel",
sideHandle = true,
content = function()
ImGui.Text("Drag the handle to move.")
controls.ButtonRow({ pop.toggleButton("tools") })
end,
})Compact floating panel with just a grip and content.
pop.popout("transport", {
title = "Transport",
style = "panel",
sideHandle = true,
showTitle = false,
content = function()
controls.ButtonRow({ pop.toggleButton("transport") })
ImGui.Text("Minimal floating panel.")
end,
})