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

@ -363,4 +363,130 @@ public class UiMenuTests
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, RawX(10), ly)));
Assert.NotNull(fired); // popup was still open -> the click landed on a real row
}
// ── Vendor dropdown polish (items 1-3): arrow indicator, popup direction,
// left alignment. See VendorUiController's "G6"/"G7"/"G8" class-doc
// paragraphs for the retail citations these three fixes are ported from.
[Fact]
public void ArrowCapSprite_FlipsBetweenClosedAndOpen_OnToggle()
{
// G6: UiMenu.CurrentArrowCapSprite projects _open onto the two
// configured sprites — the same selection DrawArrowCap makes every
// frame, exposed as a test seam since the class has no OnDraw harness.
var menu = new UiMenu
{
Width = 80f, Height = 18f,
Items = ChannelItems,
ArrowCapClosedSprite = 111u,
ArrowCapOpenSprite = 222u,
};
Assert.Equal(111u, menu.CurrentArrowCapSprite); // closed by default
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, 5))); // open
Assert.Equal(222u, menu.CurrentArrowCapSprite);
// A click in the popup's bevel ring (below every real row, iy >= InteriorH)
// closes without picking anything — the arrow must flip straight back.
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, -1)));
Assert.Equal(111u, menu.CurrentArrowCapSprite);
}
[Fact]
public void ArrowCapSprite_DefaultsToZero_NoOpForMenusThatDontAuthorOne()
{
// Chat's own controller never sets ArrowCapClosedSprite/OpenSprite (its
// arrow is baked into NormalSprite/PressedSprite already) — confirm the
// class default stays a no-op id so DrawArrowCap's `id == 0` guard fires.
var menu = new UiMenu();
Assert.Equal(0u, menu.CurrentArrowCapSprite);
Assert.Equal(0u, menu.ArrowCapClosedSprite);
Assert.Equal(0u, menu.ArrowCapOpenSprite);
}
[Fact]
public void OpenUpward_DefaultsTrue_PreservingChatsExistingUpwardGeometry()
{
// Every test above (chat's own shape) relies on this default never
// changing — G7 added the property but must not move chat's popup.
Assert.True(new UiMenu().OpenUpward);
}
[Fact]
public void OpenUpward_False_MovesThePopupBelowTheButton_NotAbove()
{
var menu = new UiMenu
{
Width = 80f, Height = 18f,
Items = ChannelItems,
Selected = (object?)ChatChannelKind.Say,
EnabledProvider = ChannelAvailable,
OpenUpward = false,
};
object? fired = null;
menu.OnSelect = p => fired = p;
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, 5))); // open
// Where "Chat to All" (index 2, Say) used to resolve when the popup
// opened UPWARD (see Select_AvailableLeftColumnItem_FiresOnSelect,
// ly=-76) is now empty space above the button. With OpenUpward=false
// this ly no longer satisfies "clicked in popup" at all, so UiMenu
// treats it as an ordinary button click and just re-closes the menu —
// it must NOT fire a selection.
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, -76)));
Assert.Null(fired);
// Re-open and click the SAME row (index 2, "Chat to All"/Say) at its
// NEW, downward position: the popup's top is now at the button's own
// bottom edge (ly = Height), not -OuterH.
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, 5))); // open
const int border = 5; // RetailChromeSprites.Border
float iy = 2 * menu.RowHeight + menu.RowHeight / 2f;
int ly = (int)(menu.Height + iy + border);
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, ly)));
Assert.Equal(ChatChannelKind.Say, fired);
}
[Fact]
public void OpenUpward_False_HitTestCoversTheButtonAndTheDownwardPopup()
{
var menu = new UiMenu { Width = 80f, Height = 18f, Items = ChannelItems, OpenUpward = false };
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, 5))); // open
// Public surface only exposes hit-testing via the protected OnHitTest,
// so exercise it the same indirect way the rest of this file does:
// a MouseDown at a point inside the downward popup's bevel is accepted
// as "in popup" (closes without firing), proving the popup geometry
// itself now lives below Height rather than above 0.
float outerH = menu.RowsPerColumn * menu.RowHeight + 2 * 5;
int belowPopupBottom = (int)(menu.Height + outerH) + 1; // just past the popup's own bottom edge
// Outside the popup entirely -> falls through to the button-toggle path.
object? fired = null;
menu.OnSelect = p => fired = p;
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, belowPopupBottom)));
Assert.Null(fired);
}
[Fact]
public void TextIndent_And_ButtonTextIndent_DefaultToChatsBakedIconOffsets()
{
// G8: these used to be private consts (19f/19f-checkbox, 20f-LED);
// converting them to settable properties must not move chat's text.
var menu = new UiMenu();
Assert.Equal(19f, menu.TextIndent);
Assert.Equal(20f, menu.ButtonTextIndent);
}
[Fact]
public void TextIndent_And_ButtonTextIndent_AreSettable_ForIconLessMenus()
{
// Vendor's controller sets both to 0 (no checkbox/LED art to clear) —
// confirm the widget actually accepts the override.
var menu = new UiMenu { TextIndent = 0f, ButtonTextIndent = 0f };
Assert.Equal(0f, menu.TextIndent);
Assert.Equal(0f, menu.ButtonTextIndent);
}
}