feat(ui): D.5.3/B.2 — spine: drag-lift hook + ghost snapshot (full opacity) + drop-on-hit-only

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-20 15:22:27 +02:00
parent a497cc631e
commit 41bb70c11a
5 changed files with 84 additions and 26 deletions

View file

@ -11,6 +11,9 @@ public class DragDropSpineTests
public bool AcceptResult = true;
public (UiItemList list, UiItemSlot cell, ItemDragPayload payload)? LastOver;
public (UiItemList list, UiItemSlot cell, ItemDragPayload payload)? LastDrop;
public (UiItemList list, UiItemSlot cell, ItemDragPayload payload)? LastLift;
public void OnDragLift(UiItemList list, UiItemSlot cell, ItemDragPayload p)
{ LastLift = (list, cell, p); }
public bool OnDragOver(UiItemList list, UiItemSlot cell, ItemDragPayload p)
{ LastOver = (list, cell, p); return AcceptResult; }
@ -125,11 +128,54 @@ public class DragDropSpineTests
}
[Fact]
public void DropReleased_notAccepted_skipsDispatch()
public void DropReleased_dispatchesToHandler_regardlessOfData0()
{
var (_, cell, h) = ListWithHandler();
// Retail model: reaching the cell means a real slot was hit (FinishDrag only delivers on a
// hit), so the handler is authoritative — it dispatches whether or not Data0 is set.
var (list, cell, h) = ListWithHandler();
cell.OnEvent(new UiEvent(0u, cell, UiEventType.DropReleased, Data0: 0, Payload: SomePayload()));
Assert.Null(h.LastDrop);
Assert.NotNull(h.LastDrop);
}
[Fact]
public void DragBegin_callsHandlerOnDragLift()
{
var (list, cell, h) = ListWithHandler();
var p = SomePayload();
cell.OnEvent(new UiEvent(0u, cell, UiEventType.DragBegin, Payload: p));
Assert.NotNull(h.LastLift);
Assert.Same(list, h.LastLift!.Value.list);
Assert.Same(cell, h.LastLift.Value.cell);
Assert.Same(p, h.LastLift.Value.payload);
}
[Fact]
public void Ghost_isSnapshottedAtBeginDrag_survivesSourceCellClearing()
{
var (root, _, cell) = RootWithBoundSlot(0x5001u); // icon tex 0x99
root.OnMouseDown(UiMouseButton.Left, 10, 10);
root.OnMouseMove(20, 10); // BeginDrag → snapshot ghost
cell.Clear(); // simulate the lift emptying the source
Assert.Equal((0x99u, 32, 32), root.DragGhostForTest);
}
[Fact]
public void FinishDrag_overNothing_deliversNoDrop_butLiftStands()
{
var root = new UiRoot { Width = 800, Height = 600 };
var list = new UiItemList(_ => (1u, 1, 1)) { Left = 0, Top = 0, Width = 32, Height = 32 };
list.Cell.Width = 32; list.Cell.Height = 32;
list.Cell.SetItem(0x5001u, 0x99u);
var h = new SpyHandler();
list.RegisterDragHandler(h);
root.AddChild(list);
root.OnMouseDown(UiMouseButton.Left, 10, 10);
root.OnMouseMove(20, 10); // BeginDrag → OnDragLift
root.OnMouseUp(UiMouseButton.Left, 600, 500); // release over empty space
Assert.NotNull(h.LastLift); // lift happened
Assert.Null(h.LastDrop); // no drop dispatched (off-bar)
Assert.Null(root.DragSource); // cleaned up
}
// ── Full UiRoot chain: arming + use-vs-drag ─────────────────────────────