fix(ui): spell-bar drag-reorder works — the per-frame rebuild was destroying the dragged cell (#354)
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
Everything already existed — the drag payloads, the favorite wire pair (0x1E3 add-at-position / 0x1E4 remove, byte-confirmed against retail's Event_AddSpellFavorite @0x006A0F70 and ACE), the insert-shift state ops. The bug: lifting a favorite fires SpellbookChanged, the next per-frame Tick rebuilt the bar, the rebuild flushed and recreated every cell, and UiRoot's subtree-removal safety net canceled the in-flight drag whose source had just been destroyed — one frame after every lift, before any drop could land. The rebuild now defers for the duration of the drag gesture, and the drop ports retail's own -1-if-lifted-before-target index adjustment (SpellCastSubMenu::AddFavorite @0x004C7060) so final positions are byte-identical: insert-shift, not swap; drag-out still deletes (the lift's removal stands on a missed drop, retail's shape). The real-pointer-pipeline test fails against the pre-fix code with the exact cancellation and passes after; a discriminator pins that physical-item drop handlers reject the spell payload. AP-172 files the one presentation divergence (mid-drag reflow happens on release, not continuously) — renumbered from the agent's AP-171 draft, which collided with the same-day double-click row. #354 filed and closed. Clean-room complete solution: 11,541 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
0a996a1a91
commit
81a9d85a1d
5 changed files with 260 additions and 5 deletions
|
|
@ -66,6 +66,7 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
private bool _disposed;
|
||||
private bool _favoritesDirty;
|
||||
private bool _endowmentDirty;
|
||||
private bool _favoriteDragActive;
|
||||
|
||||
private SpellcastingUiController(
|
||||
ImportedLayout layout,
|
||||
|
|
@ -422,16 +423,43 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
}
|
||||
|
||||
private void BeginFavoriteDrag(SpellFavoriteDragPayload payload)
|
||||
=> _removeFavorite?.Invoke(payload.SourceTab, payload.SpellId);
|
||||
{
|
||||
// gmSpellcastingUI::RecvNotice_ItemListBeginDrag @ 0x004C7360 removes the
|
||||
// lifted item from PlayerModule (+ sends the wire RemoveSpellFavorite)
|
||||
// the instant the drag starts. That removal fires SpellbookChanged, which
|
||||
// would normally set _favoritesDirty and let the next Tick() rebuild the
|
||||
// whole favorite list -- but Rebuild() flushes and recreates every slot
|
||||
// (UiItemList.Flush -> RemoveChild), and UiRoot's subtree-removal safety
|
||||
// net (ClearSubtreeOwnership) cancels any drag rooted in a removed
|
||||
// element. Left alone, a per-frame Tick() would destroy the very cell
|
||||
// driving this gesture and silently cancel the reorder before the user
|
||||
// can complete the drop. _favoriteDragActive defers the rebuild for the
|
||||
// gesture's duration; DropFavorite compensates target indices for the
|
||||
// now-stale numbering (retail's own -1-if-lifted-before-target rule,
|
||||
// ported below).
|
||||
_favoriteDragActive = true;
|
||||
_removeFavorite?.Invoke(payload.SourceTab, payload.SpellId);
|
||||
}
|
||||
|
||||
private static void EndFavoriteDrag(SpellFavoriteDragPayload payload)
|
||||
private void EndFavoriteDrag(SpellFavoriteDragPayload payload)
|
||||
{
|
||||
// The press-time command already performed retail's PlayerModule
|
||||
// removal on the one Runtime-owned Spellbook.
|
||||
// removal on the one Runtime-owned Spellbook. Release the rebuild
|
||||
// deferral -- the next Tick() resyncs the list to the (possibly
|
||||
// further-updated-by-Drop) Spellbook state.
|
||||
_favoriteDragActive = false;
|
||||
}
|
||||
|
||||
private void DropFavorite(SpellFavoriteDragPayload payload, int targetTab, int targetPosition)
|
||||
{
|
||||
// Rebuild() was deferred for the whole gesture (see BeginFavoriteDrag), so
|
||||
// every sibling slot's captured target index is still numbered against the
|
||||
// PRE-lift list. Retail's own SpellCastSubMenu::AddFavorite @ 0x004C7060
|
||||
// corrects for exactly this staleness: when the lifted item's original
|
||||
// index was before the drop target, the target index shifts down by one
|
||||
// to land where the target visually sits once the gap closes.
|
||||
if (payload.SourceTab == targetTab && payload.SourcePosition < targetPosition)
|
||||
targetPosition -= 1;
|
||||
_addFavorite?.Invoke(targetTab, targetPosition, payload.SpellId);
|
||||
_selected[targetTab] = payload.SpellId;
|
||||
}
|
||||
|
|
@ -470,7 +498,7 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
UpdateEndowment();
|
||||
SelectTab(_activeTab);
|
||||
}
|
||||
if (_favoritesDirty)
|
||||
if (_favoritesDirty && !_favoriteDragActive)
|
||||
{
|
||||
_favoritesDirty = false;
|
||||
Rebuild();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue