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

@ -822,11 +822,14 @@ public sealed class VendorUiControllerTests
// Food) -> row index 1, column 0. Derive the click point from the
// widget's own live geometry (mirrors UiMenu.OnEvent's own math)
// rather than a hardcoded pixel constant.
//
// G7: the popup opens DOWNWARD (retail authors no bool attribute 5
// on 0x100000BF — see the class doc's "G7" paragraph), so its top
// sits at the button's own bottom edge (ly = Height), not -OuterH.
const int border = 5; // RetailChromeSprites.Border (UiMenu's private bevel thickness)
float outerH = h.TypeMenu.RowsPerColumn * h.TypeMenu.RowHeight + 2 * border;
const int targetRow = 1;
float iy = targetRow * h.TypeMenu.RowHeight + h.TypeMenu.RowHeight / 2f;
float ly = iy - outerH + border;
float ly = h.TypeMenu.Height + iy + border;
Assert.True(h.TypeMenu.OnEvent(new UiEvent(0, h.TypeMenu, UiEventType.MouseDown, 0, 10, (int)ly)));
@ -834,6 +837,79 @@ public sealed class VendorUiControllerTests
Assert.Equal(FoodItemGuid, h.ItemList.GetItem(0)!.ItemId);
}
[Fact]
public void CategoryMenu_OpensDownward_NotUpward_MatchingTheAuthoredAbsentAttribute5()
{
// G7: retail's UIElement_Menu::Open (pc:120210-120252) reads a PER-MENU
// bool attribute 5 to decide direction; it defaults ABSENT to false
// (pc:106749-106778) which places the popup at the button's own bottom
// edge. Element 0x100000BF's resolved attribute bag carries no
// property "5" at all (unlike chat's, which authors it true) — so
// OpenUpward must be false here, and the OLD upward click position
// (used before this fix) must no longer resolve anything.
var h = new Harness();
h.State.Apply(VendorGuid, Profile(), new[]
{
new VendorShopItem(ArmorItemGuid, -1, 2u, "Chainmail", (uint)ItemType.Armor, 200u, 500),
new VendorShopItem(FoodItemGuid, -1, 1u, "Bread", (uint)ItemType.Food, 100u, 5),
});
Assert.False(h.TypeMenu.OpenUpward);
Assert.True(h.TypeMenu.OnEvent(new UiEvent(0, h.TypeMenu, UiEventType.MouseDown, 0, 10, 5))); // open
// The OLD (pre-fix) upward click position for row 1 ("Food") — see the
// math this same test used before G7 — now lands above the button,
// where nothing lives; UiMenu treats it as an ordinary button click
// and just re-closes the still-open menu instead of picking a row.
const int border = 5;
float outerH = h.TypeMenu.RowsPerColumn * h.TypeMenu.RowHeight + 2 * border;
const int targetRow = 1;
float iy = targetRow * h.TypeMenu.RowHeight + h.TypeMenu.RowHeight / 2f;
float oldUpwardLy = iy - outerH + border;
Assert.True(h.TypeMenu.OnEvent(new UiEvent(0, h.TypeMenu, UiEventType.MouseDown, 0, 10, (int)oldUpwardLy)));
// Selection is unchanged (still Armor, the fresh-open default) and the
// item list was not re-scoped to Food — proving the old position no
// longer hits the popup at all.
Assert.Equal("Armor", h.TypeMenu.Items.Single(i => Equals(i.Payload, h.TypeMenu.Selected)).Label);
Assert.Equal(ArmorItemGuid, h.ItemList.GetItem(0)!.ItemId);
}
[Fact]
public void CategoryMenu_ArrowCapSprites_AreWiredAndDistinctForOpenVsClosed()
{
// G6: retail's authored arrow-cap element 0x1000034E flips between its
// Normal (closed, 0x060012B1) and Highlight (open, 0x060012B2) states —
// confirm the controller actually wires both, non-zero and distinct,
// and that opening the dropdown flips CurrentArrowCapSprite the same
// way UiMenuTests exercises the mechanism generically.
var h = new Harness();
h.State.Apply(VendorGuid, Profile(), new[]
{
new VendorShopItem(ArmorItemGuid, -1, 2u, "Chainmail", (uint)ItemType.Armor, 200u, 500),
});
Assert.NotEqual(0u, h.TypeMenu.ArrowCapClosedSprite);
Assert.NotEqual(0u, h.TypeMenu.ArrowCapOpenSprite);
Assert.NotEqual(h.TypeMenu.ArrowCapClosedSprite, h.TypeMenu.ArrowCapOpenSprite);
Assert.Equal(h.TypeMenu.ArrowCapClosedSprite, h.TypeMenu.CurrentArrowCapSprite); // closed by default
Assert.True(h.TypeMenu.OnEvent(new UiEvent(0, h.TypeMenu, UiEventType.MouseDown, 0, 10, 5))); // open
Assert.Equal(h.TypeMenu.ArrowCapOpenSprite, h.TypeMenu.CurrentArrowCapSprite);
}
[Fact]
public void CategoryMenu_TextIndentsAreFlushLeft_NotChatsCheckboxLedOffsets()
{
// G8: vendor's row template (0x10000352, live-dat HJustify=Left) has
// no checkbox child and its button-label child (0x1000034D) has no
// LED art either — both indents must be 0, not chat's 19px/20px.
var h = new Harness();
Assert.Equal(0f, h.TypeMenu.TextIndent);
Assert.Equal(0f, h.TypeMenu.ButtonTextIndent);
}
[Fact]
public void CloseButton_HidesTheWindowOnly_LeavesTheSessionOpenForARefreshInPlaceReopen()
{