fix(ui): port retail item drag visuals

Keep retail's underlay-free m_pDragIcon separate from the full cell icon and reveal the authored ghost mesh on physical source cells for the complete drag lifecycle. This removes the backpack backing from the cursor and retires AP-47.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-13 09:14:24 +02:00
parent 3e84027885
commit ace5880fed
14 changed files with 307 additions and 44 deletions

View file

@ -22,6 +22,13 @@ public sealed class UiItemSlot : UiElement
/// <summary>Pre-composited icon GL texture for the bound item (0 = none).</summary>
public uint IconTexture { get; private set; }
/// <summary>
/// Underlay-free cursor graphic for the bound item (retail <c>IconData::m_pDragIcon</c>).
/// Falls back to <see cref="IconTexture"/> only for callers that have not supplied the
/// dedicated composite.
/// </summary>
public uint DragIconTexture { get; private set; }
/// <summary>
/// Lossless shortcut record when this cell belongs to the toolbar. Physical item
/// lists leave it null. Snapshotted into the drag payload before remove-on-lift.
@ -83,17 +90,39 @@ public sealed class UiItemSlot : UiElement
/// paperdoll equip slots can use their per-slot silhouettes later.</summary>
public uint EmptySprite { get; set; } = 0x060074CFu;
/// <summary>
/// Authored grey mesh shown while a physical item is being carried by drag-and-drop.
/// Retail <c>m_elem_Icon_Ghosted</c> (element 0x10000349, DirectState 0x0600109A),
/// toggled by <c>UIElement_UIItem::SetWaitingState</c> @ 0x004e11b0.
/// </summary>
public uint WaitingSprite { get; set; } = 0x0600109Au;
private bool _waiting;
internal bool WaitingVisual => _waiting;
/// <summary>RenderSurface id -> (GL texture, w, h). Set by the factory/controller.</summary>
public Func<uint, (uint tex, int w, int h)>? SpriteResolve { get; set; }
public void SetItem(uint itemId, uint iconTexture, ShortcutEntry? shortcut = null)
public void SetItem(
uint itemId,
uint iconTexture,
ShortcutEntry? shortcut = null,
uint dragIconTexture = 0)
{
ItemId = itemId;
IconTexture = iconTexture;
DragIconTexture = dragIconTexture;
Shortcut = shortcut;
}
public void Clear() { ItemId = 0; IconTexture = 0; Shortcut = null; }
public void Clear()
{
ItemId = 0;
IconTexture = 0;
DragIconTexture = 0;
Shortcut = null;
_waiting = false;
}
/// <inheritdoc/>
public override object? GetDragPayload()
@ -101,7 +130,22 @@ public sealed class UiItemSlot : UiElement
/// <inheritdoc/>
public override (uint tex, int w, int h)? GetDragGhost()
=> ItemId != 0 && IconTexture != 0 ? (IconTexture, (int)Width, (int)Height) : null;
{
if (ItemId == 0) return null;
// RenderIcons creates m_pDragIcon on a fixed 0x20 × 0x20 surface, even when its
// containing bag cell is 36 × 36. The cursor hotspot is correspondingly (16,16).
if (DragIconTexture != 0) return (DragIconTexture, 32, 32);
return IconTexture != 0 ? (IconTexture, (int)Width, (int)Height) : null;
}
/// <inheritdoc/>
internal override void SetDragSourceActive(bool active, object? payload)
{
// ItemList_BeginDrag ghosts physical lists, but explicitly excludes shortcut lists
// (along with vendor/salvage lists, which acdream does not model as ItemDragSource).
// Keep the source's full cell icon in place and reveal the authored grey mesh over it.
_waiting = active && ItemId != 0 && SourceKind != ItemDragSource.ShortcutBar;
}
/// <summary>An OCCUPIED slot is a drag source — a press-and-move picks up the item
/// rather than moving the toolbar window. An EMPTY slot is NOT a drag source, so a
@ -311,6 +355,16 @@ public sealed class UiItemSlot : UiElement
ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
}
// Pending/drag-source mesh — retail UIElement_UIItem::SetWaitingState (0x004e11b0)
// reveals m_elem_Icon_Ghosted (0x10000349). ItemList_BeginDrag (0x004e32d0)
// sets it for physical inventory/equipment cells while leaving their icon in place.
if (_waiting && SpriteResolve is not null && WaitingSprite != 0)
{
var (tex, _, _) = SpriteResolve(WaitingSprite);
if (tex != 0)
ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
}
// Drag-rollover accept/reject frame (retail SetDragAcceptState 0x10000041/40).
// Guard id != 0 BEFORE resolving — resolve(0) returns the 1×1 magenta placeholder
// with a non-zero GL handle (feedback_ui_resolve_zero_magenta).