acdream/docs/research/2026-07-13-retail-item-drag-visuals-pseudocode.md
Erik 609ed8bfc2 fix(ui): select items before drag mesh
Port ItemList_BeginDrag's select-before-waiting order so a direct press-drag shows the selection indicator on its first frame across inventory, paperdoll, and toolbar item lists.

Co-Authored-By: Codex <codex@openai.com>
2026-07-13 09:28:28 +02:00

5 KiB

Retail item drag visuals — pseudocode

Date: 2026-07-13

Scope: the cursor graphic and source-cell visual while dragging an item from an item list. This is a focused continuation of 2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md.

Retail anchors

  • IconData::RenderIcons @ 0x0058d180 (named-retail/acclient_2013_pseudo_c.txt:407524)
  • UIElement_ItemList::PrepareDragIcon @ 0x004e2a50 (named-retail/acclient_2013_pseudo_c.txt:230765)
  • UIElement_ItemList::ItemList_BeginDrag @ 0x004e32d0 (named-retail/acclient_2013_pseudo_c.txt:231350)
  • UIElement_UIItem::SetWaitingState @ 0x004e11b0 (named-retail/acclient_2013_pseudo_c.txt:229190)
  • ACCWeenieObject::SetWaitingState @ 0x0058c0d0 (named-retail/acclient_2013_pseudo_c.txt:406260)
  • UIElement_UIItem layout child m_elem_Icon_Ghosted = 0x10000349; the shared UIItem catalog 0x21000037 gives it DirectState surface 0x0600109A.

The live DAT catalog and the named retail code agree. The in-tree WorldBuilder reference has no retained UI/item-list implementation to compare for this path.

Literal pseudocode

IconData.RenderIcons(item):
    destroy old m_pIcon and m_pDragIcon

    dragSurface = transparent 32 x 32
    blit item.baseIcon onto dragSurface using normal blit
    blit item.customOverlay onto dragSurface using alpha blit
    replace pure-white dragSurface pixels from the effect-color surface
    m_pDragIcon = Graphic(dragSurface)

    cellSurface = transparent 32 x 32
    blit typeDefaultUnderlay onto cellSurface using normal blit
    blit item.customUnderlay onto cellSurface using alpha blit
    blit dragSurface onto cellSurface using alpha blit
    m_pIcon = Graphic(cellSurface)

ItemList.PrepareDragIcon(cell):
    clear cell.m_dragIcon image
    object = GetWeenieObject(cell.itemID)
    set cell.m_dragIcon image to object.GetDragIcon()  # m_pDragIcon, not m_pIcon

ItemList.ItemList_BeginDrag(cell):
    if PrepareDragIcon(cell) failed:
        return
    if cell is not selected:
        SetSelectedObject(cell.itemID, false)
    if list is not vendor, salvage, or shortcut list:
        cell.SetWaitingState(true)
    StartDragAndDrop(cell.m_dragIcon, hotspot = 16,16)

UIItem.SetWaitingState(waiting):
    object.SetWaitingState(waiting)
    if cell is not unghostable:
        set m_elem_Icon_Ghosted visible = waiting

on drag release/cancel:
    clear source waiting state
    hide m_elem_Icon_Ghosted

Destination feedback and source overlay order

UIElement_ItemList::ItemList_DragOver @ 0x004e3400 distinguishes an ordinary slot insertion from dropping into a container:

if target list is a container selector
   and target cell is occupied by a container
   and that container has an empty item slot:
    target.SetDragAcceptState(0x10000046)  # ItemSlot_DragOver_DropIn
                                           # 0x060011F7 green arrow
else if target accepts an ordinary item-list placement:
    target.SetDragAcceptState(0x10000041)  # ItemSlot_DragOver_Accept
                                           # 0x060011F9 green circle
else:
    target.SetDragAcceptState(0x10000040)  # 0x060011F8 reject

Therefore the backpack contents grid uses the green circle; the side-bag column and main-pack container cell use the green drop-in arrow. The selected/open indicators remain visible while m_elem_Icon_Ghosted is active, so the procedural leaf draws the waiting mesh before those persistent indicators and keeps drag-accept feedback topmost.

Port mapping

  • IconComposer caches the underlay-free composite separately and exposes GetDragIcon; the ordinary GetIcon still layers the two underlays beneath the same cached drag pixels.
  • UiItemSlot stores IconTexture and DragIconTexture separately. GetDragGhost returns the latter, while OnDraw keeps the former in the source cell.
  • UiRoot owns the generic source-active lifecycle, including release outside UI and source-subtree removal. It dispatches DragBegin before enabling the source-active mesh, matching retail's select-then-waiting order. The inventory, paperdoll, and toolbar handlers select an unselected dragged item synchronously. UiItemSlot maps the lifecycle to the retail waiting visual for physical lists; shortcut aliases remain unghosted.
  • UiItemSlot.WaitingSprite is the authored 0x0600109A mesh, not a procedural tint.

Conformance coverage

  • DragDropSpineTests.GetDragGhost_prefersDedicatedUnderlayFreeTexture
  • DragDropSpineTests.InventoryDrag_ghostsSourceUntilRelease
  • InventoryControllerTests.OnDragLift_selectsItem_butKeepsItUntilServerConfirms
  • PaperdollControllerTests.DragLift_selectsEquippedItem_beforeWaitingVisual
  • DragDropSpineTests.ShortcutDrag_doesNotGhostSource
  • UiItemSlotTests.DefaultWaitingSprite_isRetailGhostMesh
  • InventoryControllerTests.Empty_sprites_and_drop_feedback_are_applied_per_list_role
  • Existing IconComposerTests.TwoStageWithEffect_copiesTilePixelBeforeUnderlay locks the two-stage composition order.