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
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:
parent
5224e43890
commit
33b45ee581
5 changed files with 404 additions and 25 deletions
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue