fix(ui): port retail item drag visuals
Keep retail's underlay-free m_pDragIcon separate from the full cell icon and reveal the authored ghost mesh on physical source cells for the complete drag lifecycle. This removes the backpack backing from the cursor and retires AP-47. Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
parent
3e84027885
commit
ace5880fed
14 changed files with 307 additions and 44 deletions
|
|
@ -34,6 +34,9 @@ public sealed class IconComposer
|
|||
private readonly DatCollection _dats;
|
||||
private readonly TextureCache _cache;
|
||||
private readonly Dictionary<(uint, uint, uint, uint, uint), uint> _byTuple = new();
|
||||
private readonly Dictionary<(uint, uint, uint), ComposedIcon> _dragByTuple = new();
|
||||
|
||||
private sealed record ComposedIcon(byte[] Rgba, int Width, int Height, uint Texture);
|
||||
|
||||
// ── type-default underlay resolve (EnumIDMap 0x10000004) ─────────────────
|
||||
// Portal MasterMap (0x25000000) maps enum 0x10000004 → submap DID (0x25000008).
|
||||
|
|
@ -225,32 +228,15 @@ public sealed class IconComposer
|
|||
if (_byTuple.TryGetValue(key, out var tex)) return tex;
|
||||
|
||||
// Stage 1 — retail m_pDragIcon: base + custom overlay, then the effect recolor.
|
||||
var dragLayers = new List<(byte[] rgba, int w, int h)>();
|
||||
AddLayer(dragLayers, iconId);
|
||||
AddLayer(dragLayers, overlayId);
|
||||
(byte[] rgba, int w, int h)? drag = null;
|
||||
if (dragLayers.Count > 0)
|
||||
{
|
||||
var composed = Compose(dragLayers);
|
||||
// Effect recolor — ALWAYS, matching retail IconData::RenderIcons (0x0058d180):
|
||||
// the effect tile (enum 0x10000005, lsb(effects)+1, fallback 0x21) is non-null
|
||||
// even for effects==0 (the 0x21 SOLID-BLACK tile 0x060011C5). Retail's RenderIcons
|
||||
// calls the SURFACE overload of SurfaceWindow::ReplaceColor (0x004415b0), copying
|
||||
// the textured effect tile per-pixel into the icon's pure-white pixels — so
|
||||
// magical items take the tile's GRADIENT hue and mundane items go solid black.
|
||||
// (Visually confirmed against retail 2026-06-17: the Energy Crystal's blue is a
|
||||
// gradient, not a flat tint, and the no-mana scroll's edges are black.)
|
||||
if (TryGetEffectTile(effects, out var tile))
|
||||
ReplaceWhiteFromSurface(composed.rgba, composed.w, composed.h,
|
||||
tile.Rgba8, tile.Width, tile.Height);
|
||||
drag = composed;
|
||||
}
|
||||
// RenderIcons retains this as a distinct Graphic because the cursor ghost must not
|
||||
// carry the type/custom underlay that fills an inventory cell.
|
||||
ComposedIcon? drag = GetOrCreateDragIcon(iconId, overlayId, effects);
|
||||
|
||||
// Stage 2 — retail m_pIcon: type-default underlay (opaque) + custom underlay + drag.
|
||||
var layers = new List<(byte[] rgba, int w, int h)>();
|
||||
AddLayer(layers, typeUnderlayDid);
|
||||
AddLayer(layers, underlayId);
|
||||
if (drag is { } d) layers.Add(d);
|
||||
if (drag is not null) layers.Add((drag.Rgba, drag.Width, drag.Height));
|
||||
if (layers.Count == 0) return 0;
|
||||
|
||||
var (rgba, w, h) = Compose(layers);
|
||||
|
|
@ -259,6 +245,50 @@ public sealed class IconComposer
|
|||
return handle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve retail's dedicated cursor-drag graphic (<c>IconData::m_pDragIcon</c>): base icon
|
||||
/// + custom overlay + effect recolor, with neither the type-default nor custom underlay.
|
||||
/// <c>UIElement_ItemList::PrepareDragIcon</c> obtains exactly this graphic through
|
||||
/// <c>ACCWeenieObject::GetDragIcon</c> (0x004e2a50 / 0x0058d180).
|
||||
/// </summary>
|
||||
public uint GetDragIcon(ItemType itemType, uint iconId, uint underlayId, uint overlayId, uint effects)
|
||||
{
|
||||
// itemType/underlayId are deliberately unused: keeping the resolver signature identical
|
||||
// to GetIcon lets every item-panel binding request the two retail siblings from one model.
|
||||
_ = itemType;
|
||||
_ = underlayId;
|
||||
return iconId == 0 ? 0u : GetOrCreateDragIcon(iconId, overlayId, effects)?.Texture ?? 0u;
|
||||
}
|
||||
|
||||
private ComposedIcon? GetOrCreateDragIcon(uint iconId, uint overlayId, uint effects)
|
||||
{
|
||||
var key = (iconId, overlayId, effects);
|
||||
if (_dragByTuple.TryGetValue(key, out var cached)) return cached;
|
||||
|
||||
var dragLayers = new List<(byte[] rgba, int w, int h)>();
|
||||
AddLayer(dragLayers, iconId);
|
||||
AddLayer(dragLayers, overlayId);
|
||||
if (dragLayers.Count == 0) return null;
|
||||
|
||||
var composed = Compose(dragLayers);
|
||||
// Effect recolor — ALWAYS, matching retail IconData::RenderIcons (0x0058d180):
|
||||
// the effect tile (enum 0x10000005, lsb(effects)+1, fallback 0x21) is non-null
|
||||
// even for effects==0 (the 0x21 SOLID-BLACK tile 0x060011C5). Retail's RenderIcons
|
||||
// calls the SURFACE overload of SurfaceWindow::ReplaceColor (0x004415b0), copying
|
||||
// the textured effect tile per-pixel into the icon's pure-white pixels — so
|
||||
// magical items take the tile's GRADIENT hue and mundane items go solid black.
|
||||
// (Visually confirmed against retail 2026-06-17: the Energy Crystal's blue is a
|
||||
// gradient, not a flat tint, and the no-mana scroll's edges are black.)
|
||||
if (TryGetEffectTile(effects, out var tile))
|
||||
ReplaceWhiteFromSurface(composed.rgba, composed.w, composed.h,
|
||||
tile.Rgba8, tile.Width, tile.Height);
|
||||
|
||||
uint texture = _cache.UploadRgba8(composed.rgba, composed.w, composed.h, nearest: true);
|
||||
var created = new ComposedIcon(composed.rgba, composed.w, composed.h, texture);
|
||||
_dragByTuple[key] = created;
|
||||
return created;
|
||||
}
|
||||
|
||||
private void AddLayer(List<(byte[], int, int)> layers, uint renderSurfaceId)
|
||||
{
|
||||
if (renderSurfaceId == 0) return;
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
private readonly ClientObjectTable _objects;
|
||||
private readonly Func<uint> _playerGuid;
|
||||
private readonly Func<ItemType, uint, uint, uint, uint, uint> _iconIds;
|
||||
private readonly Func<ItemType, uint, uint, uint, uint, uint>? _dragIconIds;
|
||||
private readonly Func<int?> _strength;
|
||||
private readonly Func<string>? _ownerName;
|
||||
|
||||
|
|
@ -74,6 +75,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
ClientObjectTable objects,
|
||||
Func<uint> playerGuid,
|
||||
Func<ItemType, uint, uint, uint, uint, uint> iconIds,
|
||||
Func<ItemType, uint, uint, uint, uint, uint>? dragIconIds,
|
||||
Func<int?> strength,
|
||||
SelectionState selection,
|
||||
Func<string>? ownerName,
|
||||
|
|
@ -94,6 +96,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
_objects = objects;
|
||||
_playerGuid = playerGuid;
|
||||
_iconIds = iconIds;
|
||||
_dragIconIds = dragIconIds;
|
||||
_strength = strength;
|
||||
_ownerName = ownerName;
|
||||
_sendUse = sendUse;
|
||||
|
|
@ -214,8 +217,9 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
Action<uint, uint>? notifyMergeAttempt = null,
|
||||
ItemInteractionController? itemInteraction = null,
|
||||
Action? onClose = null,
|
||||
StackSplitQuantityState? stackSplitQuantity = null)
|
||||
=> new InventoryController(layout, objects, playerGuid, iconIds, strength, selection,
|
||||
StackSplitQuantityState? stackSplitQuantity = null,
|
||||
Func<ItemType, uint, uint, uint, uint, uint>? dragIconIds = null)
|
||||
=> new InventoryController(layout, objects, playerGuid, iconIds, dragIconIds, strength, selection,
|
||||
ownerName, datFont,
|
||||
contentsEmptySprite, sideBagEmptySprite, mainPackEmptySprite,
|
||||
sendUse, sendNoLongerViewing, sendPutItemInContainer,
|
||||
|
|
@ -315,7 +319,11 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
const uint PlayerPackBaseIcon = 0x0600127Eu; // constant main-pack backpack (visual gate)
|
||||
_topContainer.Flush();
|
||||
var main = new UiItemSlot { SpriteResolve = _topContainer.SpriteResolve };
|
||||
main.SetItem(p, _iconIds(ItemType.Container, PlayerPackBaseIcon, 0u, 0u, 0u));
|
||||
main.SetItem(
|
||||
p,
|
||||
_iconIds(ItemType.Container, PlayerPackBaseIcon, 0u, 0u, 0u),
|
||||
dragIconTexture: _dragIconIds?.Invoke(
|
||||
ItemType.Container, PlayerPackBaseIcon, 0u, 0u, 0u) ?? 0u);
|
||||
main.DragAcceptSprite = 0x060011F7u; main.DragRejectSprite = 0x060011F8u;
|
||||
main.Clicked = () =>
|
||||
{
|
||||
|
|
@ -351,8 +359,11 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
var item = _objects.Get(guid);
|
||||
uint tex = item is null ? 0u
|
||||
: _iconIds(item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects);
|
||||
uint dragTex = item is null ? 0u
|
||||
: _dragIconIds?.Invoke(
|
||||
item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects) ?? 0u;
|
||||
var cell = new UiItemSlot { SpriteResolve = list.SpriteResolve };
|
||||
cell.SetItem(guid, tex);
|
||||
cell.SetItem(guid, tex, dragIconTexture: dragTex);
|
||||
cell.SlotIndex = list.GetNumUIItems(); // index it will occupy (== its slot in a packed list)
|
||||
cell.DragAcceptSprite = 0x060011F7u; // green insert arrow (export-confirmed)
|
||||
cell.DragRejectSprite = 0x060011F8u; // red circle
|
||||
|
|
@ -400,7 +411,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
|
||||
// ── IItemListDragHandler (B-Drag) — drop an item to move it (optimistic + wire) ──────────────
|
||||
/// <summary>Inventory items do NOT lift-remove (unlike the toolbar): the item stays in its slot
|
||||
/// until the server confirms the move. Retail dims the source; we leave it + the floating ghost.</summary>
|
||||
/// while <see cref="UiItemSlot"/> reveals retail's waiting/ghosted mesh until release.</summary>
|
||||
public void OnDragLift(UiItemList sourceList, UiItemSlot sourceCell, ItemDragPayload payload) { }
|
||||
|
||||
/// <summary>Advisory neutral/accept/reject overlay. Shortcut aliases stay neutral; physical grid
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
|
|||
private readonly ClientObjectTable _objects;
|
||||
private readonly Func<uint> _playerGuid;
|
||||
private readonly Func<ItemType, uint, uint, uint, uint, uint> _iconIds;
|
||||
private readonly Func<ItemType, uint, uint, uint, uint, uint>? _dragIconIds;
|
||||
private readonly Action<uint, uint>? _sendWield; // (itemGuid, equipMask) → GetAndWieldItem 0x001A
|
||||
private readonly ItemInteractionController? _itemInteraction;
|
||||
private readonly SelectionState _selection;
|
||||
|
|
@ -96,9 +97,11 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
|
|||
Func<ItemType, uint, uint, uint, uint, uint> iconIds, SelectionState selection,
|
||||
Action<uint, uint>? sendWield,
|
||||
uint emptySlotSprite, UiDatFont? datFont, ItemInteractionController? itemInteraction,
|
||||
PaperdollClickMap? clickMap)
|
||||
PaperdollClickMap? clickMap,
|
||||
Func<ItemType, uint, uint, uint, uint, uint>? dragIconIds)
|
||||
{
|
||||
_objects = objects; _playerGuid = playerGuid; _iconIds = iconIds; _sendWield = sendWield;
|
||||
_dragIconIds = dragIconIds;
|
||||
_itemInteraction = itemInteraction;
|
||||
_selection = selection ?? throw new ArgumentNullException(nameof(selection));
|
||||
_clickMap = clickMap;
|
||||
|
|
@ -195,10 +198,11 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
|
|||
Action<uint, uint>? sendWield = null,
|
||||
uint emptySlotSprite = 0u, UiDatFont? datFont = null,
|
||||
ItemInteractionController? itemInteraction = null,
|
||||
PaperdollClickMap? clickMap = null)
|
||||
PaperdollClickMap? clickMap = null,
|
||||
Func<ItemType, uint, uint, uint, uint, uint>? dragIconIds = null)
|
||||
=> new PaperdollController(
|
||||
layout, objects, playerGuid, iconIds, selection, sendWield, emptySlotSprite,
|
||||
datFont, itemInteraction, clickMap);
|
||||
datFont, itemInteraction, clickMap, dragIconIds);
|
||||
|
||||
private void HandleDollClick(int x, int y)
|
||||
{
|
||||
|
|
@ -256,7 +260,9 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
|
|||
|
||||
if (worn is null) { list.Cell.Clear(); continue; }
|
||||
uint tex = _iconIds(worn.Type, worn.IconId, worn.IconUnderlayId, worn.IconOverlayId, worn.Effects);
|
||||
list.Cell.SetItem(worn.ObjectId, tex);
|
||||
uint dragTex = _dragIconIds?.Invoke(
|
||||
worn.Type, worn.IconId, worn.IconUnderlayId, worn.IconOverlayId, worn.Effects) ?? 0u;
|
||||
list.Cell.SetItem(worn.ObjectId, tex, dragIconTexture: dragTex);
|
||||
}
|
||||
ApplySelectionIndicators();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
private readonly CombatState? _combatState;
|
||||
private readonly Func<IReadOnlyList<ShortcutEntry>> _shortcuts;
|
||||
private readonly Func<ItemType, uint, uint, uint, uint, uint> _iconIds; // (itemType, icon, underlay, overlay, effects) → GL tex
|
||||
private readonly Func<ItemType, uint, uint, uint, uint, uint>? _dragIconIds;
|
||||
private readonly Action<uint> _useItem; // guid → fire UseObject
|
||||
private readonly ShortcutStore _store = new();
|
||||
private bool _storeLoaded;
|
||||
|
|
@ -107,12 +108,14 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
Func<uint>? selectedObjectId = null,
|
||||
Func<uint>? playerGuid = null,
|
||||
Action<uint, uint, int>? sendPutItemInContainer = null,
|
||||
UiDatFont? ammoFont = null)
|
||||
UiDatFont? ammoFont = null,
|
||||
Func<ItemType, uint, uint, uint, uint, uint>? dragIconIds = null)
|
||||
{
|
||||
_repo = repo;
|
||||
_combatState = combatState;
|
||||
_shortcuts = shortcuts;
|
||||
_iconIds = iconIds;
|
||||
_dragIconIds = dragIconIds;
|
||||
_useItem = useItem;
|
||||
_peaceDigits = peaceDigits;
|
||||
_warDigits = warDigits;
|
||||
|
|
@ -322,12 +325,14 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
Func<uint>? selectedObjectId = null,
|
||||
Func<uint>? playerGuid = null,
|
||||
Action<uint, uint, int>? sendPutItemInContainer = null,
|
||||
UiDatFont? ammoFont = null)
|
||||
UiDatFont? ammoFont = null,
|
||||
Func<ItemType, uint, uint, uint, uint, uint>? dragIconIds = null)
|
||||
{
|
||||
var c = new ToolbarController(layout, repo, shortcuts, iconIds, useItem, combatState,
|
||||
peaceDigits, warDigits, emptyDigits, itemInteraction,
|
||||
sendAddShortcut, sendRemoveShortcut, toggleCombat, selectItem,
|
||||
selectedObjectId, playerGuid, sendPutItemInContainer, ammoFont);
|
||||
selectedObjectId, playerGuid, sendPutItemInContainer, ammoFont,
|
||||
dragIconIds);
|
||||
c.Populate();
|
||||
return c;
|
||||
}
|
||||
|
|
@ -401,7 +406,9 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
var item = _repo.Get(guid);
|
||||
if (item is null) continue; // deferred: ObjectAdded re-calls Populate
|
||||
uint tex = _iconIds(item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects);
|
||||
list.Cell.SetItem(guid, tex, entry);
|
||||
uint dragTex = _dragIconIds?.Invoke(
|
||||
item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects) ?? 0u;
|
||||
list.Cell.SetItem(guid, tex, entry, dragTex);
|
||||
}
|
||||
|
||||
// Re-stamp slot number labels after any item change.
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ public sealed record ToolbarRuntimeBindings(
|
|||
ClientObjectTable Objects,
|
||||
Func<IReadOnlyList<ShortcutEntry>> Shortcuts,
|
||||
Func<ItemType, uint, uint, uint, uint, uint> ResolveIcon,
|
||||
Func<ItemType, uint, uint, uint, uint, uint> ResolveDragIcon,
|
||||
Action<uint> UseItem,
|
||||
CombatState Combat,
|
||||
ItemManaState ItemMana,
|
||||
|
|
@ -72,6 +73,7 @@ public sealed record InventoryRuntimeBindings(
|
|||
ClientObjectTable Objects,
|
||||
Func<uint> PlayerGuid,
|
||||
Func<ItemType, uint, uint, uint, uint, uint> ResolveIcon,
|
||||
Func<ItemType, uint, uint, uint, uint, uint> ResolveDragIcon,
|
||||
Func<int?> Strength,
|
||||
Action<uint>? SendUse,
|
||||
Action<uint>? SendNoLongerViewing,
|
||||
|
|
@ -404,7 +406,8 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
selectedObjectId: () => b.Selection.SelectedObjectId ?? 0u,
|
||||
playerGuid: b.PlayerGuid,
|
||||
sendPutItemInContainer: b.SendPutItemInContainer,
|
||||
ammoFont: _bindings.Assets.DefaultFont);
|
||||
ammoFont: _bindings.Assets.DefaultFont,
|
||||
dragIconIds: b.ResolveDragIcon);
|
||||
ToolbarInputController = new ToolbarInputController(ToolbarController, b.Selection);
|
||||
SelectedObjectController = Layout.SelectedObjectController.Bind(
|
||||
layout,
|
||||
|
|
@ -708,10 +711,12 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
b.SendPutItemInContainer, b.SendStackableSplitToContainer, b.SendStackableMerge,
|
||||
notifyMergeAttempt, b.ItemInteraction,
|
||||
() => CloseWindow(WindowNames.Inventory),
|
||||
StackSplitQuantity);
|
||||
StackSplitQuantity,
|
||||
b.ResolveDragIcon);
|
||||
PaperdollController paperdoll = PaperdollController.Bind(
|
||||
layout, b.Objects, b.PlayerGuid, b.ResolveIcon, b.Selection, b.SendWield,
|
||||
contents, _bindings.Assets.DefaultFont, b.ItemInteraction, paperdollClickMap);
|
||||
contents, _bindings.Assets.DefaultFont, b.ItemInteraction, paperdollClickMap,
|
||||
b.ResolveDragIcon);
|
||||
Host.WindowManager.AttachController(
|
||||
WindowNames.Inventory,
|
||||
new RetainedPanelControllerGroup(inventory, paperdoll));
|
||||
|
|
|
|||
|
|
@ -357,6 +357,14 @@ public abstract class UiElement
|
|||
/// <see cref="UiRoot"/> item-agnostic. Retail analog: m_dragIcon (decomp 229738).</summary>
|
||||
public virtual (uint tex, int w, int h)? GetDragGhost() => null;
|
||||
|
||||
/// <summary>
|
||||
/// Notifies the source widget when the root starts or finishes carrying its drag payload.
|
||||
/// This is a retained-widget lifecycle hook rather than item-specific root logic. Retail's
|
||||
/// item implementation uses it to show/hide <c>m_elem_Icon_Ghosted</c> around a physical
|
||||
/// item drag (<c>ItemList_BeginDrag</c> 0x004e32d0; <c>SetWaitingState</c> 0x004e11b0).
|
||||
/// </summary>
|
||||
internal virtual void SetDragSourceActive(bool active, object? payload) { }
|
||||
|
||||
/// <summary>
|
||||
/// Tooltip text for this widget. Retail fires event 0x07 after the configured
|
||||
/// hover delay (0.25 seconds by default), then queries the widget's virtual "GetString"
|
||||
|
|
|
|||
|
|
@ -22,6 +22,13 @@ public sealed class UiItemSlot : UiElement
|
|||
/// <summary>Pre-composited icon GL texture for the bound item (0 = none).</summary>
|
||||
public uint IconTexture { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Underlay-free cursor graphic for the bound item (retail <c>IconData::m_pDragIcon</c>).
|
||||
/// Falls back to <see cref="IconTexture"/> only for callers that have not supplied the
|
||||
/// dedicated composite.
|
||||
/// </summary>
|
||||
public uint DragIconTexture { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Lossless shortcut record when this cell belongs to the toolbar. Physical item
|
||||
/// lists leave it null. Snapshotted into the drag payload before remove-on-lift.
|
||||
|
|
@ -83,17 +90,39 @@ public sealed class UiItemSlot : UiElement
|
|||
/// paperdoll equip slots can use their per-slot silhouettes later.</summary>
|
||||
public uint EmptySprite { get; set; } = 0x060074CFu;
|
||||
|
||||
/// <summary>
|
||||
/// Authored grey mesh shown while a physical item is being carried by drag-and-drop.
|
||||
/// Retail <c>m_elem_Icon_Ghosted</c> (element 0x10000349, DirectState 0x0600109A),
|
||||
/// toggled by <c>UIElement_UIItem::SetWaitingState</c> @ 0x004e11b0.
|
||||
/// </summary>
|
||||
public uint WaitingSprite { get; set; } = 0x0600109Au;
|
||||
|
||||
private bool _waiting;
|
||||
internal bool WaitingVisual => _waiting;
|
||||
|
||||
/// <summary>RenderSurface id -> (GL texture, w, h). Set by the factory/controller.</summary>
|
||||
public Func<uint, (uint tex, int w, int h)>? SpriteResolve { get; set; }
|
||||
|
||||
public void SetItem(uint itemId, uint iconTexture, ShortcutEntry? shortcut = null)
|
||||
public void SetItem(
|
||||
uint itemId,
|
||||
uint iconTexture,
|
||||
ShortcutEntry? shortcut = null,
|
||||
uint dragIconTexture = 0)
|
||||
{
|
||||
ItemId = itemId;
|
||||
IconTexture = iconTexture;
|
||||
DragIconTexture = dragIconTexture;
|
||||
Shortcut = shortcut;
|
||||
}
|
||||
|
||||
public void Clear() { ItemId = 0; IconTexture = 0; Shortcut = null; }
|
||||
public void Clear()
|
||||
{
|
||||
ItemId = 0;
|
||||
IconTexture = 0;
|
||||
DragIconTexture = 0;
|
||||
Shortcut = null;
|
||||
_waiting = false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override object? GetDragPayload()
|
||||
|
|
@ -101,7 +130,22 @@ public sealed class UiItemSlot : UiElement
|
|||
|
||||
/// <inheritdoc/>
|
||||
public override (uint tex, int w, int h)? GetDragGhost()
|
||||
=> ItemId != 0 && IconTexture != 0 ? (IconTexture, (int)Width, (int)Height) : null;
|
||||
{
|
||||
if (ItemId == 0) return null;
|
||||
// RenderIcons creates m_pDragIcon on a fixed 0x20 × 0x20 surface, even when its
|
||||
// containing bag cell is 36 × 36. The cursor hotspot is correspondingly (16,16).
|
||||
if (DragIconTexture != 0) return (DragIconTexture, 32, 32);
|
||||
return IconTexture != 0 ? (IconTexture, (int)Width, (int)Height) : null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
internal override void SetDragSourceActive(bool active, object? payload)
|
||||
{
|
||||
// ItemList_BeginDrag ghosts physical lists, but explicitly excludes shortcut lists
|
||||
// (along with vendor/salvage lists, which acdream does not model as ItemDragSource).
|
||||
// Keep the source's full cell icon in place and reveal the authored grey mesh over it.
|
||||
_waiting = active && ItemId != 0 && SourceKind != ItemDragSource.ShortcutBar;
|
||||
}
|
||||
|
||||
/// <summary>An OCCUPIED slot is a drag source — a press-and-move picks up the item
|
||||
/// rather than moving the toolbar window. An EMPTY slot is NOT a drag source, so a
|
||||
|
|
@ -311,6 +355,16 @@ public sealed class UiItemSlot : UiElement
|
|||
ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
|
||||
}
|
||||
|
||||
// Pending/drag-source mesh — retail UIElement_UIItem::SetWaitingState (0x004e11b0)
|
||||
// reveals m_elem_Icon_Ghosted (0x10000349). ItemList_BeginDrag (0x004e32d0)
|
||||
// sets it for physical inventory/equipment cells while leaving their icon in place.
|
||||
if (_waiting && SpriteResolve is not null && WaitingSprite != 0)
|
||||
{
|
||||
var (tex, _, _) = SpriteResolve(WaitingSprite);
|
||||
if (tex != 0)
|
||||
ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
|
||||
}
|
||||
|
||||
// Drag-rollover accept/reject frame (retail SetDragAcceptState 0x10000041/40).
|
||||
// Guard id != 0 BEFORE resolving — resolve(0) returns the 1×1 magenta placeholder
|
||||
// with a non-zero GL handle (feedback_ui_resolve_zero_magenta).
|
||||
|
|
|
|||
|
|
@ -233,6 +233,7 @@ public sealed class UiRoot : UiElement
|
|||
Modal = null;
|
||||
if (IsWithinSubtree(DragSource, subtree))
|
||||
{
|
||||
DragSource?.SetDragSourceActive(false, DragPayload);
|
||||
DragSource = null;
|
||||
DragPayload = null;
|
||||
_dragGhost = null;
|
||||
|
|
@ -756,6 +757,7 @@ public sealed class UiRoot : UiElement
|
|||
DragSource = source;
|
||||
DragPayload = payload;
|
||||
_dragGhost = source.GetDragGhost(); // snapshot NOW — the DragBegin handler may empty the source cell
|
||||
source.SetDragSourceActive(true, payload);
|
||||
var e = new UiEvent(source.EventId, source, UiEventType.DragBegin, Payload: payload);
|
||||
source.OnEvent(in e);
|
||||
}
|
||||
|
|
@ -787,16 +789,24 @@ public sealed class UiRoot : UiElement
|
|||
|
||||
private void FinishDrag(int x, int y)
|
||||
{
|
||||
UiElement? source = DragSource;
|
||||
object? payload = DragPayload;
|
||||
|
||||
// Retail's source UIItem receives the release and hides m_elem_Icon_Ghosted
|
||||
// before the target handles the move. Keep this at the root lifecycle boundary so
|
||||
// releases over world space and non-item widgets clear the same state deterministically.
|
||||
source?.SetDragSourceActive(false, payload);
|
||||
|
||||
var (t, lx, ly) = HitTestTopDown(x, y);
|
||||
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);
|
||||
var e = new UiEvent(source!.EventId, t, UiEventType.DropReleased,
|
||||
Data1: (int)lx, Data2: (int)ly, Payload: payload);
|
||||
t.OnEvent(in e);
|
||||
}
|
||||
else if (DragPayload is { } payload)
|
||||
else if (payload is not null)
|
||||
{
|
||||
DragReleasedOutsideUi?.Invoke(payload, x, y);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue