acdream/tests/AcDream.App.Tests/UI/DragDropSpineTests.cs
Erik 1672eaa620 feat(ui): D.5.3/B.1 — UiItemSlot drag source + drop target + accept/reject overlay
- 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>
2026-06-20 12:40:53 +02:00

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);
}
}