From 9d483468d99de2e6ae3131809b7cd3b44f671621 Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 20 Jun 2026 12:34:21 +0200 Subject: [PATCH] =?UTF-8?q?feat(ui):=20D.5.3/B.1=20=E2=80=94=20drag=20payl?= =?UTF-8?q?oad=20+=20handler=20interface=20+=20UiItemList=20registration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- src/AcDream.App/UI/IItemListDragHandler.cs | 19 +++++++++ src/AcDream.App/UI/ItemDragPayload.cs | 24 +++++++++++ src/AcDream.App/UI/UiItemList.cs | 8 ++++ .../UI/DragDropSpineTests.cs | 42 +++++++++++++++++++ 4 files changed, 93 insertions(+) create mode 100644 src/AcDream.App/UI/IItemListDragHandler.cs create mode 100644 src/AcDream.App/UI/ItemDragPayload.cs create mode 100644 tests/AcDream.App.Tests/UI/DragDropSpineTests.cs diff --git a/src/AcDream.App/UI/IItemListDragHandler.cs b/src/AcDream.App/UI/IItemListDragHandler.cs new file mode 100644 index 00000000..817effa0 --- /dev/null +++ b/src/AcDream.App/UI/IItemListDragHandler.cs @@ -0,0 +1,19 @@ +namespace AcDream.App.UI; + +/// +/// A panel controller implements this and registers itself on each of its +/// s. Port of retail's m_dragHandler vtable +/// (RegisterItemListDragHandler, decomp 230461; confirmed acclient +/// 0x004a539e + the gmToolbarUI block 0x004bdd89). +/// decides the accept/reject OVERLAY only (advisory). +/// is authoritative — it performs the action, or +/// no-ops to reject. +/// +public interface IItemListDragHandler +{ + /// True ⇒ the target cell shows the accept (green) frame; false ⇒ reject (red). + bool OnDragOver(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload); + + /// Perform the drop (issue the per-panel wire action), or no-op to reject. + void HandleDropRelease(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload); +} diff --git a/src/AcDream.App/UI/ItemDragPayload.cs b/src/AcDream.App/UI/ItemDragPayload.cs new file mode 100644 index 00000000..b06728d4 --- /dev/null +++ b/src/AcDream.App/UI/ItemDragPayload.cs @@ -0,0 +1,24 @@ +namespace AcDream.App.UI; + +/// +/// Where a dragged item came from — the retail InqDropIconInfo flag +/// distinction (flags & 0xE == 0 fresh-from-inventory vs +/// flags & 4 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. +/// +public enum ItemDragSource { Inventory, ShortcutBar, Equipment, Ground } + +/// +/// 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 m_dragElement + +/// InqDropIconInfo out-params (objId/container/flags, decomp 230533). +/// SourceContainer is intentionally NOT stored: the handler resolves the +/// LIVE container via ClientObjectTable.Get(ObjId).ContainerId at drop — the +/// same container id retail reads off the dragged element, single source of truth. +/// +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 diff --git a/src/AcDream.App/UI/UiItemList.cs b/src/AcDream.App/UI/UiItemList.cs index 761d4db3..6c4ce9bf 100644 --- a/src/AcDream.App/UI/UiItemList.cs +++ b/src/AcDream.App/UI/UiItemList.cs @@ -24,6 +24,14 @@ public sealed class UiItemList : UiElement public Func? SpriteResolve { get; set; } + /// The drag handler this list routes drops to (a panel controller). + /// Null = the list accepts no drops. Retail: UIElement_ItemList::m_dragHandler. + public IItemListDragHandler? DragHandler { get; private set; } + + /// Register the panel's drag handler on this list. Retail: + /// UIElement_ItemList::RegisterItemListDragHandler (decomp 230461). + public void RegisterDragHandler(IItemListDragHandler handler) => DragHandler = handler; + /// Convenience for single-cell slots (the toolbar): the first cell. /// Valid only while the list has at least one cell; after /// (the inventory-phase rebuild path) the list is empty until diff --git a/tests/AcDream.App.Tests/UI/DragDropSpineTests.cs b/tests/AcDream.App.Tests/UI/DragDropSpineTests.cs new file mode 100644 index 00000000..b6f67bfc --- /dev/null +++ b/tests/AcDream.App.Tests/UI/DragDropSpineTests.cs @@ -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); + } +}