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:
parent
a497cc631e
commit
41bb70c11a
5 changed files with 84 additions and 26 deletions
|
|
@ -11,6 +11,12 @@ namespace AcDream.App.UI;
|
|||
/// </summary>
|
||||
public interface IItemListDragHandler
|
||||
{
|
||||
/// <summary>The drag STARTED from a cell in this list — retail's RecvNotice_ItemListBeginDrag
|
||||
/// → RemoveShortcut (decomp 0x004bd930/0x004bd450): the handler removes the lifted item from its
|
||||
/// model + wire so the source slot empties immediately. The item is "in hand" until
|
||||
/// HandleDropRelease (place) or the drag ends off-target (stays removed). No restore on cancel.</summary>
|
||||
void OnDragLift(UiItemList sourceList, UiItemSlot sourceCell, ItemDragPayload payload);
|
||||
|
||||
/// <summary>True ⇒ the target cell shows the accept (green) frame; false ⇒ reject (red).</summary>
|
||||
bool OnDragOver(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload);
|
||||
|
||||
|
|
|
|||
|
|
@ -301,6 +301,9 @@ public sealed class ToolbarController : IItemListDragHandler
|
|||
// The accept/reject gate (IsShortcutEligible) and the AddShortcut/RemoveShortcut
|
||||
// wire are Stream B.2; this stub accepts any real item and LOGS the drop (TS-33).
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void OnDragLift(UiItemList sourceList, UiItemSlot sourceCell, ItemDragPayload payload) { /* Task 3: remove + wire */ }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool OnDragOver(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload)
|
||||
=> payload.ObjId != 0;
|
||||
|
|
|
|||
|
|
@ -159,7 +159,11 @@ public sealed class UiItemSlot : UiElement
|
|||
return true;
|
||||
|
||||
case UiEventType.DragBegin:
|
||||
return true; // we're the source; payload already pulled by UiRoot
|
||||
// Notify the source list's handler so it can lift (remove + wire) — retail
|
||||
// RecvNotice_ItemListBeginDrag → RemoveShortcut. UiRoot snapshotted the ghost first.
|
||||
if (FindList() is { DragHandler: { } lh } liftList && e.Payload is ItemDragPayload lp)
|
||||
lh.OnDragLift(liftList, this, lp);
|
||||
return true;
|
||||
|
||||
case UiEventType.DragEnter: // pointer entered me mid-drag → ask the list's handler
|
||||
_dragAccept = (FindList() is { DragHandler: { } h } list
|
||||
|
|
@ -173,9 +177,7 @@ public sealed class UiItemSlot : UiElement
|
|||
|
||||
case UiEventType.DropReleased:
|
||||
_dragAccept = DragAcceptState.None;
|
||||
if (e.Data0 == 1 // accepted = dropped on a DIFFERENT element (skips self/empty)
|
||||
&& FindList() is { DragHandler: { } dh } dl
|
||||
&& e.Payload is ItemDragPayload dp)
|
||||
if (FindList() is { DragHandler: { } dh } dl && e.Payload is ItemDragPayload dp)
|
||||
dh.HandleDropRelease(dl, this, dp);
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,9 @@ public sealed class UiRoot : UiElement
|
|||
/// <summary>Current drag source (set between drag-begin and drop/cancel).</summary>
|
||||
public UiElement? DragSource { get; private set; }
|
||||
public object? DragPayload { get; private set; }
|
||||
private (uint tex, int w, int h)? _dragGhost;
|
||||
/// <summary>Snapshotted drag-ghost (tex,w,h), exposed for tests. See BeginDrag.</summary>
|
||||
internal (uint tex, int w, int h)? DragGhostForTest => _dragGhost;
|
||||
private UiElement? _lastDragHoverTarget;
|
||||
private int _pressX, _pressY;
|
||||
private bool _dragCandidate;
|
||||
|
|
@ -145,17 +148,15 @@ public sealed class UiRoot : UiElement
|
|||
ctx.EndOverlayLayer();
|
||||
}
|
||||
|
||||
/// <summary>Translucency of the cursor-following drag ghost. AP-47: we reuse the
|
||||
/// item's full composited icon at this alpha rather than retail's dedicated
|
||||
/// underlay-less m_pDragIcon.</summary>
|
||||
private const float GhostAlpha = 0.6f;
|
||||
private const float GhostAlpha = 1.0f; // retail m_dragIcon is the full icon, no fade
|
||||
|
||||
/// <summary>Paint the drag ghost at the cursor. The texture comes from the source
|
||||
/// element's <see cref="UiElement.GetDragGhost"/> so UiRoot stays item-agnostic; the
|
||||
/// ghost is NOT a tree element, so it never intercepts hit-tests.</summary>
|
||||
/// <summary>Paint the drag ghost at the cursor. The texture comes from the snapshotted
|
||||
/// ghost captured in <see cref="BeginDrag"/> so UiRoot stays item-agnostic and the ghost
|
||||
/// survives the source cell emptying on lift; the ghost is NOT a tree element, so it
|
||||
/// never intercepts hit-tests.</summary>
|
||||
private void DrawDragGhost(UiRenderContext ctx)
|
||||
{
|
||||
if (DragSource?.GetDragGhost() is not { } g || g.tex == 0) return;
|
||||
if (_dragGhost is not { } g || g.tex == 0) return;
|
||||
ctx.DrawSprite(g.tex, MouseX - g.w / 2f, MouseY - g.h / 2f, g.w, g.h,
|
||||
0f, 0f, 1f, 1f, new Vector4(1f, 1f, 1f, GhostAlpha));
|
||||
}
|
||||
|
|
@ -476,13 +477,11 @@ public sealed class UiRoot : UiElement
|
|||
|
||||
private void BeginDrag(UiElement source)
|
||||
{
|
||||
// Pull the payload from the source; a null payload (e.g. an empty item cell)
|
||||
// CANCELS the drag — retail's ItemList_BeginDrag only arms an occupied cell.
|
||||
var payload = source.GetDragPayload();
|
||||
if (payload is null) { _dragCandidate = false; return; }
|
||||
|
||||
DragSource = source;
|
||||
DragPayload = payload;
|
||||
_dragGhost = source.GetDragGhost(); // snapshot NOW — the DragBegin handler may empty the source cell
|
||||
var e = new UiEvent(source.EventId, source, UiEventType.DragBegin, Payload: payload);
|
||||
source.OnEvent(in e);
|
||||
}
|
||||
|
|
@ -515,16 +514,18 @@ public sealed class UiRoot : UiElement
|
|||
private void FinishDrag(int x, int y)
|
||||
{
|
||||
var (t, lx, ly) = HitTestTopDown(x, y);
|
||||
var target = t ?? DragSource!;
|
||||
var accepted = t is not null && t != DragSource;
|
||||
var e = new UiEvent(DragSource!.EventId, target, UiEventType.DropReleased,
|
||||
Data0: accepted ? 1 : 0,
|
||||
Data1: (int)lx, Data2: (int)ly,
|
||||
Payload: DragPayload);
|
||||
target.OnEvent(in e);
|
||||
|
||||
if (t is not null)
|
||||
{
|
||||
// Dropped on a real element — deliver DropReleased; the hit cell's handler places.
|
||||
// A non-item target's OnEvent ignores it, so an off-bar drop leaves the lift's removal.
|
||||
var e = new UiEvent(DragSource!.EventId, t, UiEventType.DropReleased,
|
||||
Data1: (int)lx, Data2: (int)ly, Payload: DragPayload);
|
||||
t.OnEvent(in e);
|
||||
}
|
||||
// else: dropped on nothing — no drop fires; the lift (drag-begin removal) stands (retail).
|
||||
DragSource = null;
|
||||
DragPayload = null;
|
||||
_dragGhost = null;
|
||||
_lastDragHoverTarget = null;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue