feat(ui): D.5.3/B.1 — drag payload + handler interface + UiItemList registration

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-20 12:34:21 +02:00
parent 24ce8a0b5e
commit 9d483468d9
4 changed files with 93 additions and 0 deletions

View file

@ -0,0 +1,19 @@
namespace AcDream.App.UI;
/// <summary>
/// A panel controller implements this and registers itself on each of its
/// <see cref="UiItemList"/>s. Port of retail's <c>m_dragHandler</c> vtable
/// (<c>RegisterItemListDragHandler</c>, decomp 230461; confirmed acclient
/// 0x004a539e + the gmToolbarUI block 0x004bdd89).
/// <para><see cref="OnDragOver"/> decides the accept/reject OVERLAY only (advisory).
/// <see cref="HandleDropRelease"/> is authoritative — it performs the action, or
/// no-ops to reject.</para>
/// </summary>
public interface IItemListDragHandler
{
/// <summary>True ⇒ the target cell shows the accept (green) frame; false ⇒ reject (red).</summary>
bool OnDragOver(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload);
/// <summary>Perform the drop (issue the per-panel wire action), or no-op to reject.</summary>
void HandleDropRelease(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload);
}

View file

@ -0,0 +1,24 @@
namespace AcDream.App.UI;
/// <summary>
/// Where a dragged item came from — the retail <c>InqDropIconInfo</c> flag
/// distinction (<c>flags &amp; 0xE == 0</c> fresh-from-inventory vs
/// <c>flags &amp; 4</c> within-list reorder) expressed as a typed enum. The drop
/// handler maps SourceKind + target back to the fresh-vs-reorder decision.
/// Decomp anchors: gmToolbarUI 0x004bd162 / 0x004bd1af; InqDropIconInfo 230533.
/// </summary>
public enum ItemDragSource { Inventory, ShortcutBar, Equipment, Ground }
/// <summary>
/// Snapshot of a drag-in-progress, taken at drag-begin (so a server move arriving
/// mid-drag can't mutate it under us). Port of retail's <c>m_dragElement</c> +
/// <c>InqDropIconInfo</c> out-params (objId/container/flags, decomp 230533).
/// <para><c>SourceContainer</c> is intentionally NOT stored: the handler resolves the
/// LIVE container via <c>ClientObjectTable.Get(ObjId).ContainerId</c> at drop — the
/// same container id retail reads off the dragged element, single source of truth.</para>
/// </summary>
public sealed record ItemDragPayload(
uint ObjId, // dragged weenie guid (retail itemID, +0x5FC)
ItemDragSource SourceKind, // what kind of slot it left
int SourceSlot, // the source cell's SlotIndex (retail m_lastShortcutNumDragged)
UiItemSlot SourceCell); // back-ref: reorder-restore / clear source state / ghost

View file

@ -24,6 +24,14 @@ public sealed class UiItemList : UiElement
public Func<uint, (uint tex, int w, int h)>? SpriteResolve { get; set; }
/// <summary>The drag handler this list routes drops to (a panel controller).
/// Null = the list accepts no drops. Retail: UIElement_ItemList::m_dragHandler.</summary>
public IItemListDragHandler? DragHandler { get; private set; }
/// <summary>Register the panel's drag handler on this list. Retail:
/// UIElement_ItemList::RegisterItemListDragHandler (decomp 230461).</summary>
public void RegisterDragHandler(IItemListDragHandler handler) => DragHandler = handler;
/// <summary>Convenience for single-cell slots (the toolbar): the first cell.
/// Valid only while the list has at least one cell; after <see cref="Flush"/>
/// (the inventory-phase rebuild path) the list is empty until <see cref="AddItem"/>

View file

@ -0,0 +1,42 @@
using AcDream.App.UI;
using Xunit;
namespace AcDream.App.Tests.UI;
public class DragDropSpineTests
{
// A spy handler used across the spine tests.
private sealed class SpyHandler : IItemListDragHandler
{
public bool AcceptResult = true;
public (UiItemList list, UiItemSlot cell, ItemDragPayload payload)? LastOver;
public (UiItemList list, UiItemSlot cell, ItemDragPayload payload)? LastDrop;
public bool OnDragOver(UiItemList list, UiItemSlot cell, ItemDragPayload p)
{ LastOver = (list, cell, p); return AcceptResult; }
public void HandleDropRelease(UiItemList list, UiItemSlot cell, ItemDragPayload p)
{ LastDrop = (list, cell, p); }
}
[Fact]
public void Payload_holdsAllFields()
{
var src = new UiItemSlot();
var p = new ItemDragPayload(0x5001u, ItemDragSource.ShortcutBar, 3, src);
Assert.Equal(0x5001u, p.ObjId);
Assert.Equal(ItemDragSource.ShortcutBar, p.SourceKind);
Assert.Equal(3, p.SourceSlot);
Assert.Same(src, p.SourceCell);
}
[Fact]
public void UiItemList_registerDragHandler_roundtrips()
{
var list = new UiItemList(_ => (0u, 0, 0));
Assert.Null(list.DragHandler);
var h = new SpyHandler();
list.RegisterDragHandler(h);
Assert.Same(h, list.DragHandler);
}
}