The green ring is retail's own art: every UIItem cell carries an authored DragAccept child (catalog 0x21000037, child 0x1000045A), and the spell bar's drag-over handler (SpellCastSubMenu::OnItemListDragOver @0x004C5990) flips it to the Accept state (0x10000040 -> surface 0x060011F9) for any spell payload. Ported through a per-slot SetDragAcceptVisual seam + a catalog DragOverAcceptance hook; other lists are untouched (null acceptance = neutral). A polarity error in our older docs (Accept/Reject state ids swapped) was corrected against three independent sources; the shipped art was always right, only the labels lied. The ring shares ONE landing computation with the drop (FavoriteDropIndex) — and that requirement exposed a genuine #354 off-by-one: the empty-tail path double-applied the -1 adjustment (retail gates it on the lift's removal @0x004C7157), landing a reordered spell second-to-last instead of last. Fixed; discriminator-verified both ways. AP-172 narrowed + its false empty-tail claim corrected. Clean-room complete solution: 11,545 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
6 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_UIItemlayout childm_elem_Icon_Ghosted = 0x10000349; the shared UIItem catalog0x21000037gives it DirectState surface0x0600109A.
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(0x10000040) # ItemSlot_DragOver_Accept
# 0x060011F9 green circle
else:
target.SetDragAcceptState(0x10000041) # ItemSlot_DragOver_Reject
# 0x060011F8 reject
Correction 2026-08-08 (spell-bar drop-ring research): the block above originally had the Accept/Reject numeric ids swapped (
0x10000041labeled Accept,0x10000040labeled reject). Three primary sources agree the true mapping isItemSlot_DragOver_Accept = 0x10000040 → 0x060011F9andItemSlot_DragOver_Reject = 0x10000041 → 0x060011F8: DatReaderWriter's retail-derivedUIStateIdenum; the legal/illegal branches ingmPaperDollUI::HandlePaperDollDragOver(AutoWearIsLegal→ 0x10000040 @ 0x004A3AC9, else 0x10000041 @ 0x004A3AEB) andVendorSellUI::OnItemListDragOver(DragItemAcceptable→ 0x10000040 @ 0x004C2327, else 0x10000041 @ 0x004C2336); and the machine layout dump (2026-06-25-retail-ui-layout-dump.json: state 268435520 = 0x10000040 → image 0x060011F9, state 268435521 = 0x10000041 → 0x060011F8). The art-per-semantic mapping in the shipped code was always correct; only the numeric labels here were swapped.
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
IconComposercaches the underlay-free composite separately and exposesGetDragIcon; the ordinaryGetIconstill layers the two underlays beneath the same cached drag pixels.UiItemSlotstoresIconTextureandDragIconTextureseparately.GetDragGhostreturns the latter, whileOnDrawkeeps the former in the source cell.UiRootowns the generic source-active lifecycle, including release outside UI and source-subtree removal. It dispatchesDragBeginbefore enabling the source-active mesh, matching retail's select-then-waiting order. The inventory, paperdoll, and toolbar handlers select an unselected dragged item synchronously.UiItemSlotmaps the lifecycle to the retail waiting visual for physical lists; shortcut aliases remain unghosted.UiItemSlot.WaitingSpriteis the authored0x0600109Amesh, not a procedural tint.
Conformance coverage
DragDropSpineTests.GetDragGhost_prefersDedicatedUnderlayFreeTextureDragDropSpineTests.InventoryDrag_ghostsSourceUntilReleaseInventoryControllerTests.OnDragLift_selectsItem_butKeepsItUntilServerConfirmsPaperdollControllerTests.DragLift_selectsEquippedItem_beforeWaitingVisualDragDropSpineTests.ShortcutDrag_doesNotGhostSourceUiItemSlotTests.DefaultWaitingSprite_isRetailGhostMeshInventoryControllerTests.Empty_sprites_and_drop_feedback_are_applied_per_list_role- Existing
IconComposerTests.TwoStageWithEffect_copiesTilePixelBeforeUnderlaylocks the two-stage composition order.