fix(ui): vendor dropdown polish — authored arrow-cap with open/closed flip, downward popup, left-aligned rows
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

Three gate findings, each settled by authored data rather than
invention: the button face is retail's two-piece assembly and the
17x19 arrow-cap 0x1000034E now renders with its authored
Normal(closed)/Highlight(open) states; the popup direction is an
AUTHORED attribute (UIElement_Menu::Open pc:120210-120252 — bool
attr 5, chat authors upward=true, the vendor menu authors nothing and
defaults downward), so both menus are now byte-faithful with no
special case; and the 19/20px text indents were chat-specific
checkbox/LED clearances the vendor rows don't author — measured
against the live retail font, "Spell Components" overflowed by 11px
and now fits with 8px to spare. Chat's menu defaults are bit-identical
and its tests untouched.

AP-161's arrow-cap note closes. #351 files the pre-existing FarLoad
Debug flake (three sightings today, never in clean-room Release).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-08 10:59:59 +02:00
parent 5224e43890
commit 33b45ee581
5 changed files with 404 additions and 25 deletions

View file

@ -176,18 +176,68 @@ public sealed class VendorUiController : IRetainedPanelController
/// literally what vendor's OWN button-face child (<c>0x1000034D</c>)
/// resolves to in the fixture, and <c>0x060012B4</c> (the row
/// template's "Highlight" state) is the paired open/pressed look. This
/// is a deliberate, bounded choice: retail's actual closed-button
/// chrome is a separate two-piece label+arrow assembly
/// (<c>0x1000034D</c> label + <c>0x1000034E</c> a 17x19 arrow cap with
/// its own Normal/Highlight sprites <c>0x060012B1</c>/<c>0x060012B2</c>),
/// and <see cref="UiMenu"/>'s existing single-texture 3-slice
/// <c>DrawButtonFace</c> (end caps sized for chat's 46px LED-arrow
/// texture) does not model a second overlaid arrow icon. Reworking
/// <c>DrawButtonFace</c> into a two-piece label+arrow renderer would
/// have meant redesigning UiMenu's draw model for a purely cosmetic
/// gain while the actual reported bug (nothing renders at all) is
/// fixed by this simpler substitution; the arrow glyph gap is recorded
/// as a residual visual approximation (see the register, AP-161).
/// remains a deliberate, bounded choice — <see cref="UiMenu"/>'s
/// single-texture 3-slice <c>DrawButtonFace</c> (end caps sized for
/// chat's 46px LED-arrow texture) is not redesigned into a two-piece
/// label+arrow renderer for this plain gold body.
/// </para>
/// <para>
/// <b>G6 (vendor gate finding, item 1 — the missing green arrow
/// indicator): closes the arrow-cap half of AP-161's residual.</b>
/// Retail's actual closed-button chrome IS a separate two-piece
/// label+arrow assembly — label <c>0x1000034D</c> (100x18, HJustify
/// LEFT, verified via the fixture) + a SEPARATE 17x19 arrow-cap image
/// <c>0x1000034E</c> docked at the button's right edge (X=100,Y=0
/// within the 117-wide button), with its own Normal/Highlight states
/// resolving to <c>0x060012B1</c> (closed)/<c>0x060012B2</c> (open) —
/// verified live via <c>tools/VendorLayoutScan</c>'s <c>resolved</c>
/// command reading the element's two StateMedia images directly out of
/// <c>client_local_English.dat</c>. <see cref="UiMenu.ArrowCapClosedSprite"/>/
/// <see cref="UiMenu.ArrowCapOpenSprite"/> draw this AS AN OVERLAY on top
/// of the existing 3-slice body (additive, not a DrawButtonFace rewrite)
/// so the green arrow now flips down (closed)/up (open) exactly like
/// retail, without touching chat's face (whose arrow is baked into its
/// own texture and never sets these).
/// </para>
/// <para>
/// <b>G7 (vendor gate finding, item 2 — the popup opened upward instead
/// of downward).</b> Retail's <c>UIElement_Menu::Open</c>
/// (<c>pc:120210-120252</c>, <c>0x0046cc30</c>) places the popup at
/// <c>ScreenY1(button)</c> (the button's own BOTTOM edge — i.e. BELOW
/// it) unless the menu authors bool attribute 5 true, in which case it
/// places it at <c>ScreenY0(button) - popupHeight</c> (ABOVE).
/// <c>UIElement::GetAttribute_Bool</c> (<c>pc:106749-106778</c>) defaults
/// an ABSENT attribute to false. Element <c>0x100000BF</c>'s resolved
/// attribute bag (the same fixture cited throughout this doc) carries NO
/// property "5" at all — unlike chat's channel menu (<c>0x10000014</c> in
/// LayoutDesc <c>0x21000006</c>), which explicitly authors property "5" =
/// <c>true</c>. So retail opens vendor's dropdown DOWNWARD and chat's
/// UPWARD — two different authored choices, not one hardcoded direction.
/// <see cref="UiMenu.OpenUpward"/> (default <c>true</c>, preserving
/// chat's untouched behavior) is set <c>false</c> here to match.
/// Retail's <c>Open</c> has no dynamic screen-edge clamp or flip of its
/// own — the direction is the fixed authored attribute, full stop — so
/// none is added here either; see <see cref="UiMenu.OpenUpward"/>'s own
/// doc for the citation.
/// </para>
/// <para>
/// <b>G8 (vendor gate finding, item 3 — popup/button text overflowing
/// instead of sitting flush left).</b> <see cref="UiMenu.TextIndent"/>
/// (19px) and <see cref="UiMenu.ButtonTextIndent"/> (20px) are CHAT's
/// own authored offsets — clearing space for a baked-in row checkbox and
/// a button-face LED socket, respectively. Vendor's row template
/// (<c>0x10000352</c>, live-dat verified <c>HJustify=Left</c>) has no
/// checkbox child at all, and vendor's button label child
/// (<c>0x1000034D</c>) is itself <c>HJustify=Left</c> at X=0 with no LED
/// art — so both indents are set to 0 here, retail's own left-justified
/// <c>UiText</c> convention for an icon-less label (every OTHER
/// dat-driven <c>UiText</c> in this codebase sets <c>Padding=0f</c>).
/// Reusing chat's 19px row indent measurably overflowed the authored
/// 100px-wide row: the longest category label, "Spell Components",
/// measures 92px at the default retail font (<c>Font 0x40000000</c>,
/// live-measured via <c>tools/VendorLayoutScan measure</c>) —
/// 19+92=111px &gt; the 100px row, an 11px overflow; with 0px it fits
/// with 8px to spare.
/// </para>
/// </summary>
private const int TypeMenuRowsPerColumn = 6;
@ -197,6 +247,10 @@ public sealed class VendorUiController : IRetainedPanelController
private const uint TypeMenuItemHighlightSprite = 0x060012B4u;
private const uint TypeMenuNormalSprite = 0x060012B3u;
private const uint TypeMenuPressedSprite = 0x060012B4u;
// G6: the separate arrow-cap overlay (element 0x1000034E) — see the
// class doc's "G6" paragraph above.
private const uint TypeMenuArrowCapClosedSprite = 0x060012B1u;
private const uint TypeMenuArrowCapOpenSprite = 0x060012B2u;
// G5 (vendor gate finding): the popup's docked scrollbar (element
// 0x10000351, verified via tools/VendorLayoutScan against the live dat —
@ -413,6 +467,22 @@ public sealed class VendorUiController : IRetainedPanelController
_typeMenu.ScrollThumbBottomSprite = TypeMenuScrollThumbBottomSprite;
_typeMenu.ScrollUpSprite = TypeMenuScrollUpSprite;
_typeMenu.ScrollDownSprite = TypeMenuScrollDownSprite;
// G6: the separate open/closed arrow-cap overlay — see the class
// doc's "G6" paragraph above.
_typeMenu.ArrowCapClosedSprite = TypeMenuArrowCapClosedSprite;
_typeMenu.ArrowCapOpenSprite = TypeMenuArrowCapOpenSprite;
// G7: retail authors NO bool attribute 5 on this menu, so
// UIElement_Menu::Open's absent-defaults-false path opens the popup
// DOWNWARD (ScreenY1, the button's own bottom edge) — see the class
// doc's "G7" paragraph above. Chat's own menu is unaffected (its
// controller never touches this property, leaving UiMenu's true
// default).
_typeMenu.OpenUpward = false;
// G8: vendor's row/button-label children carry no checkbox/LED art
// to clear (unlike chat's), so both indents are flush-left (0) —
// see the class doc's "G8" paragraph above.
_typeMenu.TextIndent = 0f;
_typeMenu.ButtonTextIndent = 0f;
_typeMenu.OnSelect = payload =>
{
if (payload is uint mask) SelectCategory(mask);