- UiElement: two new virtuals GetDragPayload()/GetDragGhost() (default null) keep UiRoot item-agnostic; any leaf can opt into drag by overriding these. - UiItemSlot: SlotIndex + SourceKind properties for payload identity; two overrides return ItemDragPayload / icon ghost when the slot is occupied. FindList() walks the parent chain to locate the owning UiItemList and its registered IItemListDragHandler. - UiItemSlot.OnEvent: MouseDown now just consumes the press; use-item fires on Click (mouse-up) so a drag doesn't also trigger the use-item callback. DragEnter → ask handler, set Accept/Reject overlay. DragOver → reset to None (fires on leave). DropReleased → clear overlay + dispatch to handler when Data0 == 1 (accepted). DragBegin consumed (source). - OnDraw: accept/reject sprite overlay drawn last, guarded on id != 0 to avoid the resolve(0)-→-magenta footgun. - ToolbarControllerTests: Click_emitsUseForBoundItem changed from MouseDown to Click to match the new dispatch. - 12 new DragDropSpineTests pass; full suite 481/483 (2 pre-existing skips). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
134 lines
4.8 KiB
C#
134 lines
4.8 KiB
C#
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);
|
|
}
|
|
|
|
// ── UiItemSlot drag-source payload/ghost ────────────────────────────────
|
|
[Fact]
|
|
public void GetDragPayload_emptyCell_isNull()
|
|
=> Assert.Null(new UiItemSlot().GetDragPayload());
|
|
|
|
[Fact]
|
|
public void GetDragPayload_boundCell_snapshotsFields()
|
|
{
|
|
var cell = new UiItemSlot { SlotIndex = 4, SourceKind = ItemDragSource.ShortcutBar };
|
|
cell.SetItem(0x5001u, 0x99u);
|
|
var p = Assert.IsType<ItemDragPayload>(cell.GetDragPayload());
|
|
Assert.Equal(0x5001u, p.ObjId);
|
|
Assert.Equal(ItemDragSource.ShortcutBar, p.SourceKind);
|
|
Assert.Equal(4, p.SourceSlot);
|
|
Assert.Same(cell, p.SourceCell);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetDragGhost_emptyCell_isNull()
|
|
=> Assert.Null(new UiItemSlot().GetDragGhost());
|
|
|
|
[Fact]
|
|
public void GetDragGhost_boundCell_returnsIconTuple()
|
|
{
|
|
var cell = new UiItemSlot { Width = 32, Height = 32 };
|
|
cell.SetItem(0x5001u, 0x99u);
|
|
var g = cell.GetDragGhost();
|
|
Assert.NotNull(g);
|
|
Assert.Equal(0x99u, g!.Value.tex);
|
|
Assert.Equal(32, g.Value.w);
|
|
Assert.Equal(32, g.Value.h);
|
|
}
|
|
|
|
// ── cell drop-target: DragEnter overlay + DropReleased dispatch ──────────
|
|
private static (UiItemList list, UiItemSlot cell, SpyHandler h) ListWithHandler()
|
|
{
|
|
var list = new UiItemList(_ => (1u, 1, 1)); // non-zero resolve so overlay draw is harmless
|
|
var h = new SpyHandler();
|
|
list.RegisterDragHandler(h);
|
|
return (list, list.Cell, h);
|
|
}
|
|
|
|
private static ItemDragPayload SomePayload()
|
|
=> new(0x5001u, ItemDragSource.ShortcutBar, 0, new UiItemSlot());
|
|
|
|
[Fact]
|
|
public void DragEnter_setsAcceptOverlay_whenHandlerAccepts()
|
|
{
|
|
var (_, cell, h) = ListWithHandler();
|
|
h.AcceptResult = true;
|
|
cell.OnEvent(new UiEvent(0u, cell, UiEventType.DragEnter, Payload: SomePayload()));
|
|
Assert.Equal(UiItemSlot.DragAcceptState.Accept, cell.DragAcceptVisual);
|
|
}
|
|
|
|
[Fact]
|
|
public void DragEnter_setsRejectOverlay_whenHandlerRejects()
|
|
{
|
|
var (_, cell, h) = ListWithHandler();
|
|
h.AcceptResult = false;
|
|
cell.OnEvent(new UiEvent(0u, cell, UiEventType.DragEnter, Payload: SomePayload()));
|
|
Assert.Equal(UiItemSlot.DragAcceptState.Reject, cell.DragAcceptVisual);
|
|
}
|
|
|
|
[Fact]
|
|
public void DragOver_resetsOverlayToNeutral()
|
|
{
|
|
var (_, cell, h) = ListWithHandler();
|
|
cell.OnEvent(new UiEvent(0u, cell, UiEventType.DragEnter, Payload: SomePayload()));
|
|
cell.OnEvent(new UiEvent(0u, cell, UiEventType.DragOver, Payload: SomePayload()));
|
|
Assert.Equal(UiItemSlot.DragAcceptState.None, cell.DragAcceptVisual);
|
|
}
|
|
|
|
[Fact]
|
|
public void DropReleased_accepted_dispatchesToHandler()
|
|
{
|
|
var (list, cell, h) = ListWithHandler();
|
|
var p = SomePayload();
|
|
cell.OnEvent(new UiEvent(0u, cell, UiEventType.DropReleased, Data0: 1, Payload: p));
|
|
Assert.NotNull(h.LastDrop);
|
|
Assert.Same(list, h.LastDrop!.Value.list);
|
|
Assert.Same(cell, h.LastDrop.Value.cell);
|
|
Assert.Same(p, h.LastDrop.Value.payload);
|
|
}
|
|
|
|
[Fact]
|
|
public void DropReleased_notAccepted_skipsDispatch()
|
|
{
|
|
var (_, cell, h) = ListWithHandler();
|
|
cell.OnEvent(new UiEvent(0u, cell, UiEventType.DropReleased, Data0: 0, Payload: SomePayload()));
|
|
Assert.Null(h.LastDrop);
|
|
}
|
|
}
|