Skip to content

dragdrop

CyanideX edited this page Aug 5, 2026 · 1 revision

DragDrop

Drag-and-drop reordering for ImGui lists. Items stay in place during drag; a line indicator shows where the drop will land.

Minimal Implementation

local wu = GetMod("WindowUtils")
local dd = wu.DragDrop

local items = { "First", "Second", "Third" }

dd.list("myList", items, function(item, index)
    ImGui.Button(item, ImGui.GetContentRegionAvail(), 0)
end)

Basics

list(id, items, renderFn, onReorder?, opts?)

Render a reorderable list with all state and mutation handled internally.

dd.list("myList", items, function(item, index)
    ImGui.Button(item.name, ImGui.GetContentRegionAvail(), 0)
end, function(from, to)
    print("Moved " .. from .. " -> " .. to)
end)
Parameter Type Description
id string Unique list ID
items table Array to reorder in-place on drop
renderFn function function(item, index) - render each item
onReorder function|nil function(fromIndex, toIndex) - called after reorder
opts table|nil Configuration (see below)

Returns nothing. Renders nothing when items is nil or empty. All opts are re-read every frame.

Options

Option Type Default Description
direction string "vertical" "horizontal" or "vertical"
color table {0.0, 1.0, 0.7, 1.0} Indicator line color {r,g,b,a}
thickness number 8 Line thickness in pixels
rounding number 16 Corner rounding in pixels (uses rounded rect when > 0)
spacing number ItemSpacing.y (horiz) / 4 (vert) Gap between items in pixels. Horizontal mode defaults to ImGui's ItemSpacing.y for tight layout.
padding number 0 Extends indicator line beyond item bounds in pixels
dragAlpha number 0.4 Opacity of the item being dragged (0=invisible, 1=no dim)
showHandle boolean false Show a drag handle icon before each item
handleIcon string "DragVertical" IconGlyphs key for the drag handle, or a literal glyph string if the key is not found
handleColor table {0.5, 0.5, 0.5, 1.0} Handle icon color {r,g,b,a}

With Drag Handle

dd.list("handleList", items, function(item, index)
    ImGui.Button(item.name, ImGui.GetContentRegionAvail(), 0)
end, onReorder, {
    showHandle = true,
    handleColor = { 0.2, 0.8, 0.5, 0.7 },
    color = { 0.0, 1.0, 0.7, 1.0 },
    thickness = 3,
    rounding = 2,
})

Horizontal Layout

dd.list("buttons", items, function(item, index)
    controls.Button("  " .. item .. "  ", "inactive")
end, onReorder, {
    direction = "horizontal",
})

The module handles SameLine() between items in horizontal mode. Do not add SameLine() inside renderFn.

Advanced Usage

For full manual control over the drag-drop lifecycle.

Manual Control Flow

local state = dd.createState()
local items = { "Alpha", "Beta", "Gamma" }

-- Each frame:
dd.beginFrame(state)

for i = 1, #items do
    ImGui.BeginGroup()
    ImGui.Button(items[i])
    dd.checkDragSource(state, i)
    ImGui.EndGroup()
    dd.collectRect(state, i)
end

dd.update(state, items, {
    direction = "vertical",
    color = { 1.0, 0.5, 0.0, 1.0 },
    thickness = 3,
    rounding = 4,
    onReorder = function(from, to)
        print("Reordered!")
    end,
})

createState()

Create a new drag-drop state object.

beginFrame(state)

Reset rect collection for a new frame. Call before rendering items.

collectRect(state, index)

Record the last ImGui item's screen rect. Call after each item (or group) is rendered. Does not initiate drags - use checkDragSource for that.

Call it for every index from 1 to your item count, every frame, with no gaps. update walks the whole range up to the highest index it has seen and reads a rect for each one, so a skipped index means either a stale position or an error on a rect that was never collected. That rules out clipping and conditional rendering in a reorderable list.

checkDragSource(state, index)

Check if the last ImGui item initiates a drag. Call on the interactive element that should be the drag handle (a Button, Selectable, etc.). Must be called before collectRect for the same index.

update(state, items, opts?)

Compute drop position, draw the indicator, and apply reorder on mouse release. Mutates items in place. Cheap to call every frame: it returns immediately when no drag is active.

No indicator is drawn when the drop would land the item back where it started (either side of the source), and in that case the release is swallowed without calling onReorder.

Returns: boolean - true if a reorder was applied this frame.

Options are the same as list() plus:

Option Type Description
onReorder function|nil Callback after reorder (replaces the separate parameter in list())

isDragging(state)

Whether a drag is active or just completed (suppresses click-through for one frame after release).

Returns: boolean

reorderArray(array, fromIndex, toIndex)

Move an element in an array from one index to another (mutates in-place). Returns nothing, and silently does nothing when either index is out of range or the two are equal.

destroy(id)

Remove internal state for a list ID. Call when dynamically created lists are removed.

Other Information

The indicator is drawn on the foreground draw list, so it renders above all content. When rounding > 0, the indicator is drawn as a rounded rectangle instead of a line, giving it capsule-shaped ends.

The renderFn must include at least one interactive ImGui element (Button, InvisibleButton) as the last item for drag detection. Selectable does not hold active state during drag in CET's ImGui and will not work as a drag source. Use showHandle = true if your content uses non-button widgets.

Clone this wiki locally