fix(client): restore retail interaction parity
Harden keyboard and camera routing, inventory and vendor interactions, chat/emotes, relog portal flow, and paperdoll rendering. Add retail research, connected gate coverage, and release-gate validation.
This commit is contained in:
parent
0c699240e0
commit
f6fe0f2a4f
151 changed files with 10162 additions and 1211 deletions
|
|
@ -184,6 +184,12 @@ public sealed class UiRoot : UiElement
|
|||
/// <summary>Widget currently receiving keyboard events.</summary>
|
||||
public UiElement? KeyboardFocus { get; private set; }
|
||||
|
||||
// The dispatcher is attached before retained UI. A semantic binding can
|
||||
// therefore focus chat before this tree receives the same native key.
|
||||
// Suppress that exact key through KeyChar/KeyUp so it cannot immediately
|
||||
// submit the newly-focused field or insert a rebound printable key.
|
||||
private int? _suppressedPhysicalKey;
|
||||
|
||||
/// <summary>The edit control activated by Tab/Enter when nothing is focused — retail's
|
||||
/// chat input "write mode" toggle. Set by the host once the chat window is built.</summary>
|
||||
public UiElement? DefaultTextInput { get; set; }
|
||||
|
|
@ -497,7 +503,18 @@ public sealed class UiRoot : UiElement
|
|||
|
||||
internal void OnSubtreeRemoving(UiElement subtree)
|
||||
{
|
||||
ClearSubtreeOwnership(subtree);
|
||||
// Inventory/external-container lists rebuild procedurally when an
|
||||
// authoritative object update arrives. That rebuild removes each old
|
||||
// UIItem before adding its replacement. Once BeginDrag has promoted
|
||||
// the gesture, however, retail's UIElementManager owns a separate
|
||||
// root-level drag element (StartDragandDrop @ 0x0045E040) and transfers
|
||||
// mouse capture to it; the source list cell is no longer the gesture's
|
||||
// lifetime owner. Our drag ghost is likewise snapshotted/root-owned,
|
||||
// so preserve it when the exact source leaf is replaced mid-drag and
|
||||
// transfer capture to this root. Removing a containing subtree (window
|
||||
// teardown) still cancels normally.
|
||||
bool replacingActiveDragSource = ReferenceEquals(subtree, DragSource);
|
||||
ClearSubtreeOwnership(subtree, preserveDetachedDrag: replacingActiveDragSource);
|
||||
WindowManager.OnSubtreeRemoving(subtree);
|
||||
}
|
||||
|
||||
|
|
@ -511,13 +528,16 @@ public sealed class UiRoot : UiElement
|
|||
internal void OnElementVisibilityChanged(UiElement element, bool visible)
|
||||
=> ElementVisibilityChanged?.Invoke(element, visible);
|
||||
|
||||
internal void ClearSubtreeOwnership(UiElement subtree)
|
||||
internal void ClearSubtreeOwnership(UiElement subtree, bool preserveDetachedDrag = false)
|
||||
{
|
||||
if (IsWithinSubtree(KeyboardFocus, subtree))
|
||||
SetKeyboardFocus(null);
|
||||
if (IsWithinSubtree(Captured, subtree))
|
||||
{
|
||||
ReleaseCapture();
|
||||
if (preserveDetachedDrag && ReferenceEquals(Captured, DragSource))
|
||||
SetCapture(this);
|
||||
else
|
||||
ReleaseCapture();
|
||||
_dragCandidate = false;
|
||||
}
|
||||
if (IsWithinSubtree(DefaultTextInput, subtree))
|
||||
|
|
@ -527,10 +547,13 @@ public sealed class UiRoot : UiElement
|
|||
if (IsWithinSubtree(DragSource, subtree))
|
||||
{
|
||||
DragSource?.SetDragSourceActive(false, DragPayload);
|
||||
DragSource = null;
|
||||
DragPayload = null;
|
||||
_dragGhost = null;
|
||||
_dragCandidate = false;
|
||||
if (!preserveDetachedDrag)
|
||||
{
|
||||
DragSource = null;
|
||||
DragPayload = null;
|
||||
_dragGhost = null;
|
||||
_dragCandidate = false;
|
||||
}
|
||||
}
|
||||
if (IsWithinSubtree(_hoverWidget, subtree))
|
||||
{
|
||||
|
|
@ -1090,13 +1113,15 @@ public sealed class UiRoot : UiElement
|
|||
|
||||
public void OnKeyDown(int vk, uint lparam = 0)
|
||||
{
|
||||
if (_suppressedPhysicalKey == vk)
|
||||
return;
|
||||
|
||||
// Nothing focused yet: Tab or Enter enters "write mode" by focusing the chat
|
||||
// input (retail's chat-activation hotkeys). Consumed so the same press doesn't
|
||||
// also fall through to a game hotkey.
|
||||
if (KeyboardFocus is null && DefaultTextInput is not null
|
||||
&& (vk == (int)Silk.NET.Input.Key.Tab
|
||||
|| vk == (int)Silk.NET.Input.Key.Enter
|
||||
|| vk == (int)Silk.NET.Input.Key.KeypadEnter))
|
||||
|| vk == (int)Silk.NET.Input.Key.Enter))
|
||||
{
|
||||
SetKeyboardFocus(DefaultTextInput);
|
||||
return;
|
||||
|
|
@ -1125,6 +1150,11 @@ public sealed class UiRoot : UiElement
|
|||
|
||||
public void OnKeyUp(int vk, uint lparam = 0)
|
||||
{
|
||||
if (_suppressedPhysicalKey == vk)
|
||||
{
|
||||
_suppressedPhysicalKey = null;
|
||||
return;
|
||||
}
|
||||
if (KeyboardFocus is not null)
|
||||
{
|
||||
var e = new UiEvent(KeyboardFocus.EventId, KeyboardFocus, UiEventType.KeyUp,
|
||||
|
|
@ -1136,12 +1166,18 @@ public sealed class UiRoot : UiElement
|
|||
|
||||
public void OnChar(int codepoint)
|
||||
{
|
||||
if (_suppressedPhysicalKey is not null)
|
||||
return;
|
||||
if (KeyboardFocus is null || !KeyboardFocus.IsEditControl) return;
|
||||
var e = new UiEvent(KeyboardFocus.EventId, KeyboardFocus, UiEventType.Char,
|
||||
Data0: codepoint);
|
||||
BubbleEvent(KeyboardFocus, in e);
|
||||
}
|
||||
|
||||
/// <summary>Suppress the raw retained-UI tail of a semantic key action.</summary>
|
||||
public void SuppressPhysicalKeyUntilRelease(Silk.NET.Input.Key key)
|
||||
=> _suppressedPhysicalKey = (int)key;
|
||||
|
||||
// ── Focus + capture ─────────────────────────────────────────────────
|
||||
|
||||
public void SetKeyboardFocus(UiElement? e)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue