docs(D.5.3/B.1): drag-drop spine design spec
Stream B.1 of the 2026-06-18 handoff — the shared widget-level drag-drop infra that both shortcut-drag (B) and the inventory window (C) sit on. Four design decisions confirmed with the user this session: 1. Payload = typed ItemDragPayload record snapshotted at drag-begin (ObjId/SourceKind/SourceSlot/SourceCell); SourceContainer derived at drop from ClientObjectTable (single source of truth). 2. Cursor ghost painted by UiRoot via a generic UiElement.GetDragGhost() hook — keeps UiRoot item-agnostic; floats above all windows. 3. Cell (UiItemSlot) is the drop-target hit unit + accept/reject overlay owner, delegating the decision + dispatch UP to its parent UiItemList's registered IItemListDragHandler (faithful to retail cell->ItemList_DragOver ->m_dragHandler; scales to the inventory N-cell grid). 4. PR ships infra + a visible toolbar STUB handler (logs, no wire) so the ghost/overlay/dispatch are confirmable this session; AddShortcut/Remove wire is Stream B.2. Retail-grounded: InqDropIconInfo flags (&0xE==0 fresh / &4 reorder, reject state 0x10000040) confirmed live at gmToolbarUI 0x004bd162; the cell begin-drag/CatchDroppedItem/RegisterItemListDragHandler chain at decomp 229344/229744/230461. Planned register rows: AP-47 (ghost reuses full icon at reduced alpha vs retail m_pDragIcon) + TS-33 (toolbar drop stub pending B.2). No new wire format in the spine itself. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
31d7ffd253
commit
2de9cc1c19
1 changed files with 367 additions and 0 deletions
367
docs/superpowers/specs/2026-06-20-d2b-drag-drop-spine-design.md
Normal file
367
docs/superpowers/specs/2026-06-20-d2b-drag-drop-spine-design.md
Normal file
|
|
@ -0,0 +1,367 @@
|
|||
# D.2b drag-drop spine (Stream B.1) — design
|
||||
|
||||
**Date:** 2026-06-20
|
||||
**Phase:** D.2b retail-UI engine → D.5 core panels. Stream **B.1** of the
|
||||
2026-06-18 handoff (the shared drag-drop infra that BOTH the shortcut-drag stream
|
||||
(B) and the inventory window (C) sit on). Build it once, well — it is the
|
||||
critical-path lynchpin.
|
||||
**Branch:** `claude/hopeful-maxwell-214a12` (== main == `31d7ffd`).
|
||||
**Spec author:** lead-engineer brainstorm 2026-06-20 (4 design decisions confirmed
|
||||
by the user).
|
||||
|
||||
**Read alongside:**
|
||||
- `docs/research/2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md` §5 / §5.7
|
||||
(the pseudocode IS the spec; sub-element + flag anchors).
|
||||
- `docs/research/2026-06-18-d53-bar-finish-and-inventory-handoff.md` §B.1
|
||||
(current-code readiness).
|
||||
- `claude-memory/project_d2b_retail_ui.md` (the toolkit), `feedback_ui_resolve_zero_magenta`
|
||||
(the 0-id → magenta footgun; guard on the id, not the GL handle).
|
||||
|
||||
---
|
||||
|
||||
## 1. Goal & non-goals
|
||||
|
||||
**Goal.** Complete the widget-level drag-drop machine so a player can pick up an
|
||||
item icon, see a translucent ghost track the cursor, watch hovered slots flip to
|
||||
**accept (green) / reject (red)**, and drop — at which point the drop is dispatched
|
||||
to the owning panel's handler with a fully-resolved payload. `UiRoot` already holds
|
||||
the *device-level* drag state machine (`BeginDrag`/`UpdateDragHover`/`FinishDrag`,
|
||||
promoted on >3 px move, live-wired to Silk.NET). This stream supplies the five
|
||||
missing pieces (handoff §B.1): payload injection, the cursor ghost, drop-target
|
||||
hooks on the cell, the accept/reject overlay, and the panel-handler interface.
|
||||
|
||||
**In scope (the spine — generic, shared):**
|
||||
1. Payload injection into `UiRoot.BeginDrag` (today passes `payload: null`).
|
||||
2. A cursor-following drag ghost, painted by `UiRoot`, item-agnostic.
|
||||
3. Drop-target hooks on `UiItemSlot` (DragEnter/Over/DropReleased → accept/reject
|
||||
overlay + dispatch).
|
||||
4. The `IItemListDragHandler` interface + registration on `UiItemList`.
|
||||
5. A typed `ItemDragPayload` describing what is dragged and where from.
|
||||
6. Moving the item-cell's use-trigger from MouseDown → Click (drag/click
|
||||
disambiguation; also more retail-faithful).
|
||||
7. A **visible toolbar stub handler** so the chain is verifiable this session.
|
||||
|
||||
**Out of scope (explicitly deferred — later streams):**
|
||||
- The `AddShortcut 0x019C` / `RemoveShortcut 0x019D` wire and the mutable
|
||||
`ShortcutStore` (Stream **B.2**).
|
||||
- The inventory window, `UiItemList` N-cell grid mode, window manager
|
||||
(Streams **C / window-mgr**).
|
||||
- Stack-split drag (the entry/slider `0x100001A3/A4`), give-to-NPC, drop-to-ground,
|
||||
wield wire — all per-panel **HandleDropRelease** opcode selection (the deep-dive
|
||||
§5.7 opcode table; lands with each consuming panel).
|
||||
- The faithful `m_pDragIcon` (base+overlay-no-underlay) drag composite — MVP reuses
|
||||
the existing full icon at reduced alpha (AP-47).
|
||||
- Esc-to-cancel-drag (retail has it; future polish).
|
||||
|
||||
---
|
||||
|
||||
## 2. Retail grounding (the four confirmed decisions)
|
||||
|
||||
The named decomp (`acclient_2013_pseudo_c.txt`) was greped to anchor each piece;
|
||||
the spine introduces **no new wire format** (opcodes are B.2/C), so the workflow's
|
||||
grep→cross-ref→pseudocode step applies to the *event-chain semantics* and
|
||||
`InqDropIconInfo`, both confirmed below.
|
||||
|
||||
### 2.1 The retail chain (deep-dive §5.1–5.5)
|
||||
- The cell `UIElement_UIItem` is BOTH drag source and drop target. On
|
||||
left-press-and-move it walks to its parent `UIElement_ItemList` and calls
|
||||
`ItemList_BeginDrag` (`ListenToElementMessage` decomp 229344, msg `0x21`).
|
||||
- Every cell is a drop target *by construction* — `PostInit` sets the
|
||||
`CatchDroppedItem` attribute `0x36` true (decomp 229744).
|
||||
- On drag-over the cell forwards to `ItemList_DragOver`; the LIST routes to its
|
||||
registered `m_dragHandler` (`RegisterItemListDragHandler`, decomp 230461;
|
||||
confirmed at acclient 0x004a539e + the gmToolbarUI block 0x004bdd89).
|
||||
- `InqDropIconInfo(dragElement, &objId, &containerId, &flags)` reads the dragged
|
||||
element's properties (decomp 230533); the **flags** word (confirmed live at
|
||||
`gmToolbarUI` 0x004bd162 / 0x004bd1af): **`flags & 0xE == 0`** ⇒ fresh-from-
|
||||
inventory; **`flags & 4`** ⇒ within-list reorder. Accept/reject overlay state ids:
|
||||
neutral `0x1000003f`, **reject `0x10000040`**, accept `0x10000041`
|
||||
(`SetDragAcceptState`, confirmed 0x004bd16d).
|
||||
- The drag ghost `m_dragIcon` (id `0x10000345`) is a root-level translucent copy of
|
||||
the icon, tracking the cursor (decomp 229738; §5.5).
|
||||
|
||||
### 2.2 How the four picks map (all confirmed with the user)
|
||||
| # | Decision | Pick | Retail basis |
|
||||
|---|---|---|---|
|
||||
| 1 | Payload shape | typed `ItemDragPayload` record, snapshotted at begin | mirrors `InqDropIconInfo`'s {objId, container, flags}; our `flags` derived at drop from SourceKind+target |
|
||||
| 2 | Ghost render | painted by `UiRoot` via a generic `GetDragGhost()` hook | retail's root-level `m_dragElement` floats above all windows |
|
||||
| 3 | Drop unit | **cell** hits + shows overlay; **list** owns the handler | retail cell→`ItemList_DragOver`→`m_dragHandler` |
|
||||
| 4 | PR scope | infra + a **visible toolbar stub** handler | so the ghost/overlay/dispatch are confirmable now; wire is B.2 |
|
||||
|
||||
---
|
||||
|
||||
## 3. Architecture — components & boundaries
|
||||
|
||||
All code lives in `src/AcDream.App/UI/` (the retail UI tree; NOT the ImGui devtools
|
||||
path). No new project references; `UiRoot` stays item-agnostic (it learns the
|
||||
payload/ghost only through two `UiElement` virtuals).
|
||||
|
||||
```
|
||||
Silk.NET mouse ─► UiRoot (device drag machine; UNCHANGED chain, + payload pull + ghost)
|
||||
│ BeginDrag(source): payload = source.GetDragPayload(); cancel if null
|
||||
│ Draw(): paint source.GetDragGhost() at the cursor (overlay layer)
|
||||
▼
|
||||
UiItemSlot (the CELL — drag source + drop target + overlay owner)
|
||||
│ GetDragPayload() → ItemDragPayload | null (null = empty cell, no drag)
|
||||
│ GetDragGhost() → (tex,w,h) | null
|
||||
│ OnEvent: Click→use; DragEnter→ask handler→overlay; DropReleased→dispatch
|
||||
▼ (walks Parent to)
|
||||
UiItemList (the GRID — owns the registered handler)
|
||||
│ DragHandler : IItemListDragHandler
|
||||
▼
|
||||
IItemListDragHandler (a panel controller implements this)
|
||||
bool OnDragOver(list, cell, payload) → accept/reject (overlay only)
|
||||
void HandleDropRelease(list, cell, payload) → the action (wire = per panel)
|
||||
▲
|
||||
ToolbarController (registers itself; STUB this stream — logs, no wire = TS-33)
|
||||
```
|
||||
|
||||
**Boundary rules honored:** Rule 1 (no fat feature body in `GameWindow` — all logic
|
||||
in the UI classes + the controller); Rule 3 (panels target the toolkit, never a
|
||||
backend); `UiRoot` depends on nothing item-specific (Rule-2-spirit: the generic
|
||||
toolkit core doesn't reach up into the item layer — it pulls through virtuals).
|
||||
|
||||
---
|
||||
|
||||
## 4. New & changed types (precise signatures)
|
||||
|
||||
### 4.1 NEW — `ItemDragPayload` + `ItemDragSource` (`UI/ItemDragPayload.cs`)
|
||||
```csharp
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>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: gmToolbarUI 0x004bd162 / 0x004bd1af.</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 m_dragElement +
|
||||
/// InqDropIconInfo out-params (objId/container/flags, decomp 230533).
|
||||
/// SourceContainer is NOT stored — the handler resolves the LIVE container from
|
||||
/// ClientObjectTable.Get(ObjId).ContainerId at drop (single source of truth; same
|
||||
/// value retail reads off the element, see §7).</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 texture
|
||||
```
|
||||
|
||||
### 4.2 CHANGED — `UiElement` (two new virtuals; default null)
|
||||
```csharp
|
||||
/// <summary>The data this element carries when a drag begins. UiRoot.BeginDrag pulls
|
||||
/// this; a NULL return CANCELS the drag (retail: ItemList_BeginDrag only arms an
|
||||
/// occupied cell). Default null = not draggable.</summary>
|
||||
public virtual object? GetDragPayload() => null;
|
||||
|
||||
/// <summary>The texture UiRoot paints at the cursor while this element is the drag
|
||||
/// source: (GL handle, width, height). Null = no ghost. Keeps UiRoot item-agnostic.</summary>
|
||||
public virtual (uint tex, int w, int h)? GetDragGhost() => null;
|
||||
```
|
||||
|
||||
### 4.3 CHANGED — `UiRoot`
|
||||
- `BeginDrag(UiElement source)` (drop the `payload` param): pull
|
||||
`var payload = source.GetDragPayload();` → if `null`, set `_dragCandidate = false`
|
||||
and **return without arming** (no `DragSource`, no event). Else set
|
||||
`DragSource`/`DragPayload`, fire `DragBegin` with the payload. Call site (line 188)
|
||||
becomes `BeginDrag(Captured);`.
|
||||
- `Draw(ctx)`: after `DrawOverlays`, still inside the overlay layer, draw the ghost:
|
||||
```csharp
|
||||
if (DragSource?.GetDragGhost() is { tex: var t, w: var gw, h: var gh } && t != 0)
|
||||
ctx.DrawSprite(t, MouseX - gw/2f, MouseY - gh/2f, gw, gh, 0,0,1,1,
|
||||
new Vector4(1,1,1, GhostAlpha)); // GhostAlpha = 0.6f
|
||||
```
|
||||
Root transform is at origin during `Draw`, so cursor coords are absolute. The
|
||||
ghost is NOT a tree element → it never intercepts hit-tests.
|
||||
|
||||
### 4.4 CHANGED — `UiItemSlot`
|
||||
- Add `public int SlotIndex { get; set; } = -1;` (the cell's own index within its
|
||||
panel — 0..17 toolbar, container slot for inventory; distinct from `ShortcutNum`,
|
||||
the 1–9 LABEL which is -1 on the bottom row).
|
||||
- Add `public ItemDragSource SourceKind { get; set; } = ItemDragSource.Inventory;`
|
||||
(controller overrides; toolbar = `ShortcutBar`).
|
||||
- Add drag-accept overlay sprites (confirmed ids; configurable; guard `id != 0`
|
||||
before resolving — the magenta footgun):
|
||||
```csharp
|
||||
public uint DragAcceptSprite { get; set; } = 0x060011F9u; // ItemSlot_DragOver_Accept
|
||||
public uint DragRejectSprite { get; set; } = 0x060011F8u; // ItemSlot_DragOver_Reject
|
||||
private enum DragAccept { None, Accept, Reject }
|
||||
private DragAccept _dragAccept = DragAccept.None;
|
||||
```
|
||||
- `GetDragPayload()` → `ItemId != 0 ? new ItemDragPayload(ItemId, SourceKind, SlotIndex, this) : null`.
|
||||
- `GetDragGhost()` → `ItemId != 0 && IconTexture != 0 ? (IconTexture, (int)Width, (int)Height) : null`.
|
||||
- **Move use from MouseDown → Click** (the disambiguation fix): `OnEvent` keeps
|
||||
`MouseDown` → `return true` (still consume the press, but DON'T fire use), and adds
|
||||
`Click` (0x01) → `Clicked?.Invoke(); return true`. (`UiRoot` already suppresses the
|
||||
post-drag Click at line 311, so a completed drag won't also use the item; a
|
||||
press-release-without-move still emits Click → use.) `WireClick` in
|
||||
`ToolbarController` is unchanged — it still assigns `Clicked`.
|
||||
- `OnEvent` new cases (acdream's UiEventType is the contract — these are acdream's
|
||||
DragEnter/DragOver/DropReleased, NOT retail's raw per-cell 0x21/0x3e/0x15 codes;
|
||||
see §6 note):
|
||||
```csharp
|
||||
case UiEventType.DragBegin: return true; // we're the source; payload already pulled
|
||||
case UiEventType.DragEnter: // pointer entered me mid-drag
|
||||
_dragAccept = (FindList() is {} l && l.DragHandler is {} h
|
||||
&& e.Payload is ItemDragPayload p && h.OnDragOver(l, this, p))
|
||||
? DragAccept.Accept : DragAccept.Reject;
|
||||
return true;
|
||||
case UiEventType.DragOver: _dragAccept = DragAccept.None; return true; // UiRoot fires on LEAVE
|
||||
case UiEventType.DropReleased:
|
||||
_dragAccept = DragAccept.None;
|
||||
if (e.Data0 == 1 // accepted = dropped on a DIFFERENT element (skips self/empty)
|
||||
&& FindList() is {} dl && dl.DragHandler is {} dh
|
||||
&& e.Payload is ItemDragPayload dp)
|
||||
dh.HandleDropRelease(dl, this, dp);
|
||||
return true;
|
||||
```
|
||||
`FindList()` walks `Parent` until a `UiItemList`. The accept/reject overlay draws
|
||||
in `OnDraw` on top of the icon+digit, `id != 0` guarded.
|
||||
|
||||
### 4.5 NEW — `IItemListDragHandler` (`UI/IItemListDragHandler.cs`)
|
||||
```csharp
|
||||
/// <summary>A panel controller implements this and registers itself on each of its
|
||||
/// UiItemLists. Port of retail's m_dragHandler vtable (RegisterItemListDragHandler,
|
||||
/// decomp 230461). OnDragOver decides the accept/reject OVERLAY only (advisory);
|
||||
/// HandleDropRelease is authoritative — it does the action (or no-ops to reject).</summary>
|
||||
public interface IItemListDragHandler
|
||||
{
|
||||
bool OnDragOver(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload);
|
||||
void HandleDropRelease(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload);
|
||||
}
|
||||
```
|
||||
|
||||
### 4.6 CHANGED — `UiItemList`
|
||||
- Add `public IItemListDragHandler? DragHandler { get; private set; }` +
|
||||
`public void RegisterDragHandler(IItemListDragHandler h) => DragHandler = h;`.
|
||||
|
||||
### 4.7 CHANGED — `ToolbarController` (the stub handler — TS-33)
|
||||
- `: IItemListDragHandler`. In the ctor, after caching slots, set each cell's
|
||||
`SlotIndex = i` and `SourceKind = ItemDragSource.ShortcutBar`, and
|
||||
`list.RegisterDragHandler(this)`.
|
||||
- `OnDragOver` → `payload.ObjId != 0` (accept any real item; retail's
|
||||
`IsShortcutEligible` gate is B.2).
|
||||
- `HandleDropRelease` → `Console.WriteLine($"[B.2 TODO] drop obj 0x{payload.ObjId:X8} " +
|
||||
$"from {payload.SourceKind} slot {payload.SourceSlot} → toolbar slot {targetCell.SlotIndex}");`
|
||||
No store mutation, no wire. (TS-33; replaced wholesale in B.2.)
|
||||
|
||||
---
|
||||
|
||||
## 5. Data flow — frame by frame
|
||||
|
||||
1. **Press** on an occupied toolbar slot → `UiRoot.OnMouseDown` sets `Captured = cell`,
|
||||
`_dragCandidate = true` (cell isn't `CapturesPointerDrag`, isn't a window-drag).
|
||||
2. **Move > 3 px** → `UiRoot.OnMouseMove` calls `BeginDrag(cell)` → pulls
|
||||
`cell.GetDragPayload()` = `ItemDragPayload{ObjId, ShortcutBar, srcSlot, cell}`
|
||||
(non-null since occupied) → arms `DragSource`/`DragPayload`, fires `DragBegin`.
|
||||
3. **Each move while dragging** → `UpdateDragHover(x,y)` hit-tests; on target CHANGE
|
||||
it fires `DragOver` (LEAVE) on the old cell → its overlay resets to neutral, and
|
||||
`DragEnter` on the new cell → it asks its list's handler `OnDragOver` → overlay
|
||||
flips accept/reject. Meanwhile `UiRoot.Draw` paints the ghost at the cursor.
|
||||
4. **Release** → `UiRoot.OnMouseUp`: `DragSource` set → `FinishDrag(x,y)` delivers
|
||||
`DropReleased` to the cell under the cursor (`Data0 = 1` if a different element).
|
||||
The cell → its list's `HandleDropRelease(list, cell, payload)` → (stub) logs.
|
||||
`DragSource`/`DragPayload` cleared → ghost stops. Capture released.
|
||||
5. **Empty-slot drag attempt** → step 2 `GetDragPayload()` returns null →
|
||||
`_dragCandidate=false`, nothing armed; the eventual mouse-up emits Click (no-op,
|
||||
`Clicked` guards `ItemId != 0`).
|
||||
|
||||
---
|
||||
|
||||
## 6. Edge cases & notes
|
||||
|
||||
- **Event-id namespace.** acdream's `UiEventType` (DragBegin 0x15, DragEnter 0x21,
|
||||
DragOver 0x1C, DropReleased 0x3E) is the contract the cell handles — these are
|
||||
`UiRoot`'s constants and differ from retail's raw per-cell `ListenToElementMessage`
|
||||
codes (0x21 begin / 0x3e over / 0x15 drop). Do NOT try to match the retail per-cell
|
||||
codes; the SEMANTICS match, the numbers are `UiRoot`'s. (IA-12 already covers "UI
|
||||
toolkit mirrors behavior, not byte-layout.")
|
||||
- **Hover re-eval cadence.** `UiRoot.UpdateDragHover` early-returns when the target
|
||||
is unchanged, so accept/reject is computed once per cell-enter, not every move.
|
||||
For item cells the decision is constant per (cell, payload), so this is
|
||||
behavior-equivalent to retail's per-move `MouseOverTop` — no register row.
|
||||
- **Self-drop / drop-on-nothing.** `FinishDrag` sets `Data0=0` when the target is the
|
||||
source or null; the cell gates `HandleDropRelease` on `Data0==1`, so both are clean
|
||||
no-ops (= "put it back").
|
||||
- **Magenta footgun.** `DragAcceptSprite`/`DragRejectSprite` default non-zero; the
|
||||
`OnDraw` overlay still guards `if (id != 0)` before `SpriteResolve`, never on the
|
||||
returned GL handle (`feedback_ui_resolve_zero_magenta`).
|
||||
- **No heavy diagnostics in the render loop.** The stub's `Console.WriteLine` fires
|
||||
only on a discrete drop event (not per frame), so it can't eat dt-based animations.
|
||||
|
||||
---
|
||||
|
||||
## 7. Divergence register rows (add in the IMPLEMENTATION commit)
|
||||
|
||||
- **AP-47** (Documented approximation): *Drag ghost reuses the full composited
|
||||
`m_pIcon` at reduced alpha (`GhostAlpha=0.6`) instead of retail's dedicated
|
||||
`m_pDragIcon` (base + custom-overlay, NO underlay).* Where: `UiRoot.Draw` +
|
||||
`UiItemSlot.GetDragGhost`. Why safe: cosmetic only — the dragged item is still
|
||||
unambiguously identifiable; building the second composite is deferred polish. Risk:
|
||||
the ghost shows the opaque type-default underlay backing rather than retail's
|
||||
underlay-less translucent copy. Oracle: `IconData::RenderIcons` 407594-407625
|
||||
(m_pDragIcon path); deep-dive §3.2/§5.5.
|
||||
- **TS-33** (Temporary stopgap): *Toolbar `IItemListDragHandler.HandleDropRelease`
|
||||
is a logging stub — no `AddShortcut 0x019C`/`RemoveShortcut 0x019D` wire, no
|
||||
`ShortcutStore` mutation.* Where: `ToolbarController.HandleDropRelease`. Awaiting:
|
||||
Stream B.2. Risk: a drop onto the bar visibly does nothing but log; reorder/add/
|
||||
remove are inert until B.2. Oracle: `gmToolbarUI::HandleDropRelease`
|
||||
acclient_2013_pseudo_c.txt:197971.
|
||||
|
||||
**Note (not a register row):** the payload omits `SourceContainer`; the handler
|
||||
resolves the live container via `ClientObjectTable.Get(ObjId).ContainerId` at drop.
|
||||
Same container id retail reads off the dragged element — single source of truth, no
|
||||
stale snapshot → no observable behavior deviation.
|
||||
|
||||
---
|
||||
|
||||
## 8. Testing (conformance)
|
||||
|
||||
App-layer tests in `tests/AcDream.App.Tests/UI/` (no GL — pure logic):
|
||||
1. `GetDragPayload` returns null for an empty cell, a correct `ItemDragPayload` for a
|
||||
bound cell (ObjId/SourceKind/SourceSlot/SourceCell).
|
||||
2. `BeginDrag` does NOT arm (`DragSource == null`) when the source's payload is null;
|
||||
arms + fires `DragBegin` when non-null.
|
||||
3. Drop dispatch: a fake `IItemListDragHandler` registered on a `UiItemList` receives
|
||||
`OnDragOver` on DragEnter and `HandleDropRelease` on DropReleased with the right
|
||||
`(list, cell, payload)`; NOT called on a `Data0==0` self/empty drop.
|
||||
4. Accept/reject overlay state flips to Accept when the handler returns true, Reject
|
||||
when false, None on leave.
|
||||
5. Use-vs-drag: a Click (no intervening drag) fires `Clicked`; a completed drag does
|
||||
not (drive `UiRoot` MouseDown→move>3px→MouseUp and assert `Clicked` didn't fire).
|
||||
6. `FindList()` resolves the parent `UiItemList` through the cell's `Parent` chain.
|
||||
|
||||
Drive these through `UiRoot`'s public `OnMouseDown/OnMouseMove/OnMouseUp` where
|
||||
possible (integration-level), so the device chain is exercised, not just the cell in
|
||||
isolation.
|
||||
|
||||
---
|
||||
|
||||
## 9. Acceptance criteria
|
||||
|
||||
- [ ] `dotnet build` green; `dotnet test` green (existing + the new §8 tests).
|
||||
- [ ] `UiRoot` has zero compile-time dependency on `UiItemSlot`/`ItemDragPayload`
|
||||
(item-agnostic; only the two `UiElement` virtuals bridge).
|
||||
- [ ] AP-47 + TS-33 rows added in the implementation commit; counts bumped.
|
||||
- [ ] **Visual (user):** with `ACDREAM_RETAIL_UI=1`, in-world, grab a hotbar item
|
||||
and drag it: a translucent ghost follows the cursor; the hovered slot shows the
|
||||
accept (green) frame, a slot that rejects shows red; on release over another
|
||||
slot the log prints `[B.2 TODO] drop obj … → toolbar slot N`; the source item
|
||||
stays put (no wire yet). A plain click still USES the item; a drag does not.
|
||||
- [ ] Memory updated if a durable lesson emerged; roadmap/ISSUES note if needed.
|
||||
|
||||
---
|
||||
|
||||
## 10. Subagent task slices (for the plan)
|
||||
|
||||
1. **Toolkit core** — `ItemDragPayload`/`ItemDragSource`, the two `UiElement`
|
||||
virtuals, `UiRoot.BeginDrag` payload pull + cancel, `UiRoot.Draw` ghost.
|
||||
2. **Cell hooks** — `UiItemSlot`: `SlotIndex`/`SourceKind`, `GetDragPayload`/
|
||||
`GetDragGhost`, MouseDown→Click move, the DragEnter/Over/DropReleased cases +
|
||||
accept/reject overlay; `IItemListDragHandler`; `UiItemList.DragHandler`.
|
||||
3. **Toolbar stub + wiring** — `ToolbarController : IItemListDragHandler`, register +
|
||||
set SlotIndex/SourceKind, the logging `HandleDropRelease`; register rows.
|
||||
4. **Tests** — the §8 suite.
|
||||
|
||||
(Slices 1→2 are sequential; 3 depends on 2; 4 can follow each.)
|
||||
Loading…
Add table
Add a link
Reference in a new issue