Skip to content

holdbuttons

CyanideX edited this page Aug 18, 2026 · 3 revisions

Hold Buttons

Hold-to-confirm buttons that prevent accidental destructive actions, plus compound action buttons with primary click and secondary hold.

Quick Start

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

-- Hold-to-confirm delete button
local held, clicked = c.HoldButton("delete_all", "Delete All", {
    duration = 1.5, style = "danger",
    onHold = function() deleteAll() end,
    onClick = function() wu.Notify.info("Hold to confirm") end,
})

-- Action button: click to load, hold to delete
c.ActionButton("preset_1", "My Preset", {
    onPrimary = function() loadPreset(1) end,
    onSecondary = function() deletePreset(1) end,
    isActive = currentPreset == 1,
})

HoldButton(id, label, opts?)

Button that requires holding to trigger. Prevents accidental destructive actions.

Parameter Type Default Description
id string - Unique button ID
label string - Display label
opts.duration number 2.0 Hold duration in seconds
opts.style string "danger" Button style name
opts.width number 0 Button width (negative = fill)
opts.progressDisplay string "overlay" "overlay", "replace", or "external"
opts.progressStyle string "danger" Progress bar style (for replace mode)
opts.disabled boolean false Disable the button
opts.tooltip string|nil nil Tooltip text
opts.overlayColor table|nil nil Custom overlay fill color {r,g,b,a}
opts.onClick function|nil nil Callback on early release (click)
opts.onHold function|nil nil Callback on hold completion
opts.elementId string|nil nil Tutorial/bounds target ID

Returns: boolean, boolean - triggered (hold completed), clicked (released early)

Progress display modes:

  • overlay - rect fills over the button as you hold
  • replace - button is replaced by a progress bar while held
  • external - no visual on the button itself; use ShowHoldProgress() elsewhere

opts.overlayColor only applies in overlay mode. opts.progressStyle only applies in replace mode.

opts.elementId is not reported while a progressDisplay = "replace" button is actively held, because that path returns before the bounds reporting step.

Fields ButtonRow passes but HoldButton ignores

ButtonRow and ButtonBlock forward def.warningMessage into the HoldButton opts table. HoldButton never reads it, so it has no effect anywhere. Use opts.tooltip (or def.tooltip) if you want to warn the user before they commit.

local held, clicked = c.HoldButton("delete_all", "Delete All", {
    duration = 1.5, style = "danger",
    onHold = function() deleteAll() end,
    onClick = function() wu.Notify.info("Hold to confirm") end,
})

ActionButton(id, label, opts?)

Compound button: primary click + secondary hold-to-confirm with cross-element progress.

Parameter Type Default Description
opts.onPrimary function|nil - Primary click callback
opts.onSecondary function|nil - Secondary hold callback
opts.secondaryIcon string trash icon Secondary button icon
opts.secondaryDuration number 1.0 Hold duration
opts.secondaryStyle string "danger" Secondary button style
opts.progressStyle string "danger" Progress bar style
opts.style string "inactive" Primary button style
opts.isActive boolean|nil nil Override to "active" style
opts.width number|nil full width Total width
opts.elementId string|nil nil Tutorial/bounds target ID

The secondary button is only rendered when onSecondary is set. Without it the control is just a styled full-width button, and secondaryTriggered is always false. The secondary hold always runs in progressDisplay = "external" mode: the progress bar replaces the primary button rather than filling the icon.

The secondary hold button registers under the ID id .. "_secondary", which is the ID to pass to getHoldProgress or resetHoldState.

Returns: table - a fresh table { primaryClicked, secondaryTriggered } on every call

c.ActionButton("preset_1", "My Preset", {
    onPrimary = function() loadPreset(1) end,
    onSecondary = function() deletePreset(1) end,
    isActive = currentPreset == 1,
})

StatusRow(id, label, labelStyle?, actionDef?, opts?)

Non-interactive status label with an optional hold-to-confirm action icon. A named shorthand for the display-label-plus-destructive-action pattern.

When actionDef is provided, the label occupies the remaining width after the hold icon slot. When omitted, the label fills the full row width.

Parameter Type Default Description
id string - Unique ID for the hold button (only used when actionDef is provided)
label string - Display text for the label slot
labelStyle string|nil "label" Style name for the label slot
actionDef table|nil nil Optional hold action definition (see below)
opts.tooltip string|nil nil Tooltip for the label slot

actionDef fields:

Field Type Default Description
icon string|nil trash icon Icon glyph for the hold button
style string "danger" Hold button style
duration number 1.0 Hold duration in seconds
onHold function|nil nil Callback on hold completion

The hold button registers under id .. "_action". Pass that ID to resetHoldState if you need to clear it manually.

-- Label only (no action)
c.StatusRow("preset_status", "My Preset", "label")

-- Label with hold-to-delete action
c.StatusRow("preset_status", "My Preset", nil, {
    icon = IconGlyphs.TrashCanOutline,
    duration = 1.5,
    onHold = function() deletePreset() end,
})

-- Custom label style and tooltip
c.StatusRow("preset_status", "Read-only Preset", "statusbar", nil, {
    tooltip = "This preset cannot be modified",
})

Hold Progress Helpers

getHoldProgress(id)

Returns: number|nil - progress 0-1, or nil if not holding

ShowHoldProgress(sourceId, width?, progressStyle?)

Renders a progress bar showing another button's hold state. Use with progressDisplay = "external".

Returns: boolean - true if progress bar is showing

getFirstActiveHoldProgress(ids)

Returns the progress and source ID of the first actively-held button. Accepts a single ID string or an array of IDs.

Parameter Type Description
ids string | string[] Single button ID or array of IDs to check

Returns: number|nil, string|nil - progress fraction (0-1) and source button ID, or nil, nil if none are held

-- Single ID form
local progress, sourceId = c.getFirstActiveHoldProgress("my_hold_btn")

-- Array form
local progress, sourceId = c.getFirstActiveHoldProgress({ "hold_a", "hold_b", "hold_c" })
if progress then
    print("Button " .. sourceId .. " at " .. string.format("%.0f%%", progress * 100))
end

When multiple buttons are held simultaneously, the one whose ID appears first in the array wins.

ShowFirstActiveHoldProgress(ids, width?, progressStyle?)

Renders a progress bar for the first actively-held button in a list. No-op when no button is held.

Parameter Type Default Description
ids string | string[] - Single button ID or array of IDs to check
width number|nil fill Bar width in pixels (negative = fill)
progressStyle string|table|nil "danger" Style name, or a table mapping source IDs to style names

Returns: boolean - true if a progress bar was rendered

-- Single ID
c.ShowFirstActiveHoldProgress("hold_btn", -1, "danger")

-- Array with per-source styles
c.ShowFirstActiveHoldProgress(
    { "hold_a", "hold_b" }, -1,
    { hold_a = "danger", hold_b = "warning" }
)

When progressStyle is a table and the active source ID is not present as a key, the style falls back to "danger".

resetHoldState(id) / resetAllHoldStates()

Clear hold button state for cleanup.

Clone this wiki locally