merge(vt): plain <menu> popup — dark rows, selected fill, plain scrollbar, no checkmark (owner: the open dropdown was still retail art)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-07 10:21:11 +02:00
commit 22e6ae88a1
3 changed files with 472 additions and 2 deletions

View file

@ -117,7 +117,18 @@ list boxes (owner live-client report, 2026-09-07), so a plugin `<menu>` now
draws the flat VTank/Decal `HudCombo` box (list-matching fill/border, a
left-aligned value, and a small ▾) by default; `style="retail"` opts back
into the gold face for a panel that genuinely wants it. Any other value
throws `FormatException` at `Build`.
throws `FormatException` at `Build`. The plain style covers the WHOLE menu,
closed and open: a follow-up owner report (still 2026-09-07 — "Drop down
menus look horrible, there is also a checkmark on the text there") found the
OPEN popup still drew retail's tan/orange gradient panel, its ornate gold
scrollbar, and a baked checkmark glyph on the current entry even with
`style="plain"`. The open popup now matches `<list>`'s own chrome too: a
flat fill + 1px border, one row per entry in the list text color, the
current entry filled like a list selection, the hovered entry a slightly
lighter fill, and no checkmark; more entries than the row cap show a plain
1px-bordered scrollbar track with a flat thumb, no DAT scrollbar art.
`style="retail"` keeps the sprite popup (gradient panel, checkmark-bearing
row art, ornate scrollbar) exactly as before, unchanged.
Common to every element via `ApplyCommon`: `name`/`id` (a stable control
name), `visible` (literal `true`/`false` or a bound `bool` property),

View file

@ -160,6 +160,22 @@ public sealed class UiMenu : UiElement
private bool _draggingPopupThumb;
private float _popupThumbDragOffset;
/// <summary>Index into <see cref="Items"/> of the row under the pointer while
/// the plain popup is open, or -1. Presentation-only (see
/// <see cref="PlainHoverColor"/>'s doc) — retail's sprite popup has no
/// equivalent hover concept, so this never affects the retail draw path.</summary>
private int _hoveredPopupIndex = -1;
/// <summary>Test seam, same rationale as <see cref="CurrentFaceSpriteForTest"/>.</summary>
internal int HoveredPopupIndexForTest => _hoveredPopupIndex;
/// <summary>
/// The plain popup needs continuous MouseMove while open to keep its hover
/// highlight tracking the cursor (retail's sprite popup has no such state, so
/// this only matters when <see cref="RetailButtonArt"/> is false).
/// </summary>
public override bool ReceivesHoverMouseMove => _open && !RetailButtonArt;
private const int Border = RetailChromeSprites.Border; // 8-piece bevel thickness (5px)
// The row sprites 0x0600124E/4D bake a checkbox/checkmark into the leftmost ~17px
// square; the label starts just past it (box width + small gap) so text aligns with
@ -339,6 +355,28 @@ public sealed class UiMenu : UiElement
/// with the list rows beneath it.</summary>
public const float PlainPadding = 3f;
// ── Plain OPEN-popup chrome (RetailButtonArt = false). Owner live-client
// report 2026-09-07 ("Drop down menus look horrible, there is also a
// checkmark on the text there"): the S7 fix above only replaced the
// CLOSED-state button face — opening the dropdown still drew retail's
// tan/orange gradient panel (PopupBgSprite), the row-highlight sprites
// (whose art bakes a checkbox/checkmark glyph into the leftmost ~17px —
// see TextIndent's doc comment), and the ornate scrollbar chrome. VTank's
// own open combo (VVS HudCombo, docs/research/vtank-kb/08-ui-views.md §2)
// is a plain dark list — no gradient, no baked checkmark — so the plain
// popup below reuses UiMarkupList's own list palette (same rationale as
// PlainBackgroundColor/PlainBorderColor above) rather than inventing a
// third color scheme.
/// <summary>The current entry's row fill — identical value to
/// <see cref="UiMarkupList.SelectedColor"/> so a plugin's open dropdown
/// reads as the same widget family as its lists.</summary>
public Vector4 PlainSelectedColor { get; set; } = new(0.28f, 0.23f, 0.08f, 0.95f);
/// <summary>A slightly lighter fill for the row under the pointer (no
/// separate glyph or sprite swap — fills only, mirroring
/// <see cref="PlainOpenBorderColor"/>'s "tint, never a sprite swap" rule
/// for the closed state).</summary>
public Vector4 PlainHoverColor { get; set; } = new(0.40f, 0.33f, 0.14f, 0.95f);
private bool _open;
/// <summary>
@ -377,6 +415,7 @@ public sealed class UiMenu : UiElement
OnOpen?.Invoke();
}
_open = value;
_hoveredPopupIndex = -1; // stale hover from the last time this popup was open
if (FindRoot() is not { } root) return;
if (value) root.SetActivePopup(this, () => SetOpen(false));
else root.ClearActivePopup(this);
@ -617,8 +656,29 @@ public sealed class UiMenu : UiElement
/// pass) greys out the part of the popup that overlaps it.</summary>
protected override void OnDrawOverlay(UiRenderContext ctx)
{
if (!_open) return;
// Owner live-client report 2026-09-07: the S7 closed-state fix left the
// OPEN popup drawing retail's gradient/checkmark art regardless of
// RetailButtonArt. Plain mode needs no SpriteResolve at all — it draws
// only untextured fills/outlines (see DrawGridPopupPlain/
// DrawScrollablePopupPlain's own doc comments).
if (!RetailButtonArt)
{
ctx.PushAlphaAbsolute(1f);
try
{
if (Scrollable)
DrawScrollablePopupPlain(ctx);
else
DrawGridPopupPlain(ctx);
}
finally { ctx.PopAlpha(); }
return;
}
var resolve = SpriteResolve;
if (!_open || resolve is null) return;
if (resolve is null) return;
// Force OPAQUE (a menu reads solid even though the chat window is translucent).
// Draw bevel → panel fill → row sprites → labels, all through the sprite bucket
@ -772,6 +832,152 @@ public sealed class UiMenu : UiElement
}
}
// ── Plain OPEN-popup drawing (RetailButtonArt = false) ──────────────────
//
// Owner live-client report 2026-09-07: no DAT art at all — a flat fill
// background, a 1px border, one row per entry in the list text color, the
// current entry filled like a list selection, the hovered entry a slightly
// lighter fill, and NO checkmark (retail's row-highlight sprites bake a
// checkbox/checkmark glyph into their leftmost ~17px — see TextIndent's
// doc comment — which a flat DrawFill simply cannot draw, so plain mode
// has none by construction). These mirror DrawGridPopup/DrawScrollablePopup's
// shape exactly (same column/row math, same VisibleTopRow/EnabledProvider
// rules) so hit-testing (OnHitTest/OnEvent, unchanged) stays byte-identical
// to what it already computes for the retail path.
/// <summary>Plain counterpart of <see cref="DrawGridPopup"/> — flat fill +
/// 1px outline instead of the bevel/panel sprites, per-row selected/hover
/// fills instead of highlight sprites, <see cref="PlainTextColor"/>/
/// <see cref="TextColorGhosted"/> labels left-aligned at
/// <see cref="PlainPadding"/> instead of the authored <see cref="TextIndent"/>/
/// <see cref="ItemTextCentered"/> justification (plain mode has no baked
/// checkbox glyph to align past, and no authored per-menu justification
/// convention — VTank's own list rows are always left-aligned).</summary>
private void DrawGridPopupPlain(UiRenderContext ctx)
{
float outerTop = PopupTop;
float inX = Border, inY = outerTop + Border;
ctx.DrawFill(0f, outerTop, OuterW, OuterH, PlainBackgroundColor);
ctx.DrawRectOutline(0f, outerTop, OuterW, OuterH, PlainBorderColor, 1f);
for (int i = 0; i < Items.Count; i++)
{
int col = i / RowsPerColumn, row = i % RowsPerColumn;
float x = inX + col * ColumnWidth, y = inY + row * RowHeight;
bool selected = Equals(Items[i].Payload, Selected);
if (selected)
ctx.DrawFill(x, y, ColumnWidth, RowHeight, PlainSelectedColor);
else if (i == _hoveredPopupIndex)
ctx.DrawFill(x, y, ColumnWidth, RowHeight, PlainHoverColor);
}
float textY = (RowHeight - LineH()) * 0.5f;
for (int i = 0; i < Items.Count; i++)
{
int col = i / RowsPerColumn, row = i % RowsPerColumn;
bool avail = EnabledProvider?.Invoke(Items[i].Payload) ?? true;
DrawLabel(ctx, Items[i].Label, inX + col * ColumnWidth + PlainPadding,
inY + row * RowHeight + textY,
avail ? PlainTextColor : TextColorGhosted);
}
}
/// <summary>Plain counterpart of <see cref="DrawScrollablePopup"/> — same
/// <see cref="VisibleTopRow"/>-sliced single column, plain
/// selected/hover row fills, and a plain scrollbar
/// (<see cref="DrawPopupScrollbarPlain"/>) instead of the sprite chrome.</summary>
private void DrawScrollablePopupPlain(UiRenderContext ctx)
{
ConfigurePopupScroll();
float outerTop = PopupTop;
float inX = Border, inY = outerTop + Border;
ctx.DrawFill(0f, outerTop, OuterW, OuterH, PlainBackgroundColor);
ctx.DrawRectOutline(0f, outerTop, OuterW, OuterH, PlainBorderColor, 1f);
int start = VisibleTopRow;
int count = System.Math.Min(EffectiveVisibleRows, Items.Count - start);
float textY = (RowHeight - LineH()) * 0.5f;
for (int i = 0; i < count; i++)
{
int idx = start + i;
float y = inY + i * RowHeight;
bool selected = Equals(Items[idx].Payload, Selected);
if (selected)
ctx.DrawFill(inX, y, ColumnWidth, RowHeight, PlainSelectedColor);
else if (idx == _hoveredPopupIndex)
ctx.DrawFill(inX, y, ColumnWidth, RowHeight, PlainHoverColor);
}
for (int i = 0; i < count; i++)
{
int idx = start + i;
bool avail = EnabledProvider?.Invoke(Items[idx].Payload) ?? true;
DrawLabel(ctx, Items[idx].Label, inX + PlainPadding, inY + i * RowHeight + textY,
avail ? PlainTextColor : TextColorGhosted);
}
DrawPopupScrollbarPlain(ctx, inX + ColumnWidth, inY);
}
/// <summary>
/// Plain counterpart of <see cref="DrawPopupScrollbar"/>: a 1px-bordered
/// track and a flat thumb, both in <see cref="PlainBorderColor"/> — no DAT
/// thumb/track/arrow-button art at all. Shares the exact same
/// <see cref="UiScrollbar.ThumbRect"/> geometry (so the thumb's drawn
/// position matches <see cref="HandleScrollablePopupMouseDown"/>'s hit-test
/// math), but draws no separate up/down button glyphs — plain mode has no
/// art for them and the click regions already work through geometry alone
/// (<see cref="HandleScrollablePopupMouseDown"/> is unchanged).
/// </summary>
private void DrawPopupScrollbarPlain(UiRenderContext ctx, float x, float y)
{
if (!IsPopupScrollbarPresentationVisible) return;
ctx.DrawFill(x, y, ScrollbarWidth, InteriorH, PlainBackgroundColor);
ctx.DrawRectOutline(x, y, ScrollbarWidth, InteriorH, PlainBorderColor, 1f);
if (!PopupScroll.HasOverflow) return;
float decExtent = System.Math.Clamp(ScrollButtonExtent, 0f, InteriorH);
float incExtent = System.Math.Clamp(ScrollButtonExtent, 0f, InteriorH - decExtent);
float trackTop = decExtent;
float trackLen = MathF.Max(0f, InteriorH - decExtent - incExtent);
var (ty, th) = UiScrollbar.ThumbRect(PopupScroll, trackTop, trackLen);
ctx.DrawFill(x + 1f, y + ty, MathF.Max(0f, ScrollbarWidth - 2f), th, PlainBorderColor);
}
/// <summary>
/// Recomputes the hovered popup row from a MouseMove's local (lx,ly) —
/// same convention <see cref="OnEvent"/>'s MouseDown handling already uses
/// (<see cref="PopupTop"/>/<see cref="Border"/>-relative). Plain-mode-only:
/// see <see cref="ReceivesHoverMouseMove"/>'s doc comment for why this is
/// never invoked on the retail sprite-popup path.
/// </summary>
private void UpdatePlainPopupHover(float lx, float ly)
{
float ix = lx - Border, iy = ly - (PopupTop + Border);
_hoveredPopupIndex = Scrollable ? HoveredScrollableIndex(ix, iy) : HoveredGridIndex(ix, iy);
}
private int HoveredGridIndex(float ix, float iy)
{
if (ix < 0 || ix >= InteriorW || iy < 0 || iy >= InteriorH) return -1;
int col = (int)(ix / ColumnWidth);
int row = (int)(iy / RowHeight);
int idx = col * RowsPerColumn + row;
return row >= 0 && row < RowsPerColumn && idx >= 0 && idx < Items.Count ? idx : -1;
}
private int HoveredScrollableIndex(float ix, float iy)
{
if (ix < 0 || ix >= ColumnWidth || iy < 0 || iy >= InteriorH) return -1;
int row = (int)(iy / RowHeight);
int idx = VisibleTopRow + row;
return row >= 0 && row < EffectiveVisibleRows && idx >= 0 && idx < Items.Count ? idx : -1;
}
/// <summary>Draw the universal 8-piece retail window bevel (corners + tiled edges +
/// tiled centre fill) framing the rect (<paramref name="x"/>,<paramref name="y"/>,
/// <paramref name="w"/>,<paramref name="h"/>). Reuses the same geometry +
@ -846,11 +1052,25 @@ public sealed class UiMenu : UiElement
}
}
// Plain-mode hover tracking (see ReceivesHoverMouseMove's doc comment):
// continuous MouseMove while the plain popup is open recomputes the
// hovered row for DrawGridPopupPlain/DrawScrollablePopupPlain. Checked
// BEFORE the MouseUp/HoverLeave/MouseDown-only gates below since, like
// the Scrollable drag block above, it spans an event type none of them
// handle.
if (!RetailButtonArt && _open && e.Type == UiEventType.MouseMove)
{
UpdatePlainPopupHover(e.Data1, e.Data2);
return true;
}
if (e.Type is UiEventType.MouseUp
or UiEventType.HoverLeave
or UiEventType.CaptureChanged)
{
_facePressed = false; // the momentary face flick ends here
if (e.Type == UiEventType.HoverLeave)
_hoveredPopupIndex = -1;
return false;
}

View file

@ -208,4 +208,243 @@ public sealed class UiMenuPlainStyleTests
Assert.Equal(1, QuadCount(segs, FontTexture));
Assert.Equal(0, QuadCount(segs, 0u));
}
// ── OPEN-popup coverage (owner live-client report 2026-09-07: "Drop down
// menus look horrible, there is also a checkmark on the text there") ────
//
// The S7 fix above only replaced the CLOSED-state button face. The tests
// below pin the OPEN popup: plain mode draws no sprite/gradient/checkmark
// art at all (only untextured fills via DrawFill/DrawRectOutline, exactly
// like UiMarkupList's own chrome), while the retail popup — the class
// default, and every non-markup UiMenu caller — is unchanged (the
// existing golden above only covers the closed state; the golden here
// covers the open popup).
private const float PlainRowHeight = 18f;
private const float PlainColumnWidth = 90f;
private static UiMenu MakePopupMenu(
bool retailButtonArt, int itemCount, int rowsPerColumn, bool scrollable,
System.Action<int>? countResolveCall = null)
{
var items = Enumerable.Range(0, itemCount)
.Select(i => new UiMenu.MenuItem(i == 0 ? "W" : $"row{i}", (object?)i))
.ToArray();
return new UiMenu
{
Width = 100f, Height = 20f,
DatFont = MakeFont(),
// Retail tests read the texture id straight back (id => (id, w, h)) so a
// texture id is a specific sprite by construction — the same convention
// UiAncestorClipTests uses. Plain tests wrap this to prove it is NEVER
// invoked (no gradient/sprite of ANY kind, not just the ones this class
// happens to name).
SpriteResolve = id =>
{
countResolveCall?.Invoke(1);
return (id, 8, 8);
},
RetailButtonArt = retailButtonArt,
NormalSprite = 0x06004D65u,
PressedSprite = 0x06004D66u,
PopupBgSprite = 0x0600124Cu,
ItemNormalSprite = 0x0600124Eu,
ItemHighlightSprite = 0x0600124Du,
// Non-zero retail scrollbar chrome ids (UiScrollbar.cs's own doc-cited
// values) so a plain test can assert these are never resolved — a zero
// id would be indistinguishable from "never set", and would collide
// with the untextured-fill bucket's own texture-0 key.
ScrollTrackSprite = 0x06004C5Fu,
ScrollThumbSprite = 0x06004C63u,
ScrollThumbTopSprite = 0x06004C60u,
ScrollThumbBottomSprite = 0x06004C66u,
ScrollUpSprite = 0x06004C6Cu,
ScrollDownSprite = 0x06004C69u,
ColumnWidth = PlainColumnWidth,
RowHeight = PlainRowHeight,
RowsPerColumn = rowsPerColumn,
Scrollable = scrollable,
OpenUpward = false, // downward: PopupTop == Height, simplest math for these tests
Items = items,
ButtonLabelProvider = () => "W",
};
}
private static bool HasFillQuad(
System.Collections.Generic.IReadOnlyList<(uint Texture, System.Collections.Generic.IReadOnlyList<float> Verts)> segs,
float x, float y, float w, float h, Vector4 color, float tol = 0.05f)
{
foreach (var seg in segs)
{
if (seg.Texture != 0u) continue;
var v = seg.Verts;
for (int b = 0; b + FloatsPerQuad <= v.Count; b += FloatsPerQuad)
{
float qx = v[b], qy = v[b + 1];
float qw = v[b + 8] - qx, qh = v[b + 9] - qy;
float r = v[b + 4], g = v[b + 5], bl = v[b + 6], a = v[b + 7];
if (MathF.Abs(qx - x) < tol && MathF.Abs(qy - y) < tol
&& MathF.Abs(qw - w) < tol && MathF.Abs(qh - h) < tol
&& MathF.Abs(r - color.X) < tol && MathF.Abs(g - color.Y) < tol
&& MathF.Abs(bl - color.Z) < tol && MathF.Abs(a - color.W) < tol)
return true;
}
}
return false;
}
/// <summary>Opens the popup (MouseDown on the closed face) then, if given,
/// hovers a row via MouseMove — the same (Data1,Data2) local-coordinate
/// convention <see cref="UiMenu.OnEvent"/> already uses for MouseDown.</summary>
private static void OpenAndHover(UiMenu menu, int? hoverRow = null)
{
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, Data1: 10, Data2: 10)));
Assert.True(menu.IsOpen);
if (hoverRow is { } row)
{
// ix = lx - Border, iy = ly - (PopupTop + Border); PopupTop == Height (20)
// for these OpenUpward=false menus, Border == RetailChromeSprites.Border (5).
int ly = 20 + RetailChromeSprites.Border + row * (int)PlainRowHeight + (int)(PlainRowHeight / 2);
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseMove, Data1: 10, Data2: ly)));
}
}
[Fact]
public void Plain_OpenPopup_GridMode_DrawsFlatFillsSelectedAndHover_NoSpriteResolveCalls()
{
int resolveCalls = 0;
var menu = MakePopupMenu(retailButtonArt: false, itemCount: 3, rowsPerColumn: 7, scrollable: false,
countResolveCall: n => resolveCalls += n);
menu.Selected = 1; // row 1 is "current"
OpenAndHover(menu, hoverRow: 2); // row 2 is hovered (not selected)
var (renderer, ctx) = MakeContext(200f, 200f);
menu.DrawOverlays(ctx);
var segs = renderer.DebugSpriteSegmentVerts;
Assert.Equal(0, resolveCalls); // no DAT art resolved at all — not even by id
Assert.Equal(0, QuadCount(segs, 0x0600124Cu)); // retail PopupBgSprite never drawn
Assert.Equal(0, QuadCount(segs, 0x0600124Du)); // retail ItemHighlightSprite (bakes the checkmark) never drawn
Assert.Equal(0, QuadCount(segs, 0x0600124Eu)); // retail ItemNormalSprite never drawn
float outerTop = menu.Height; // OpenUpward=false
float outerW = menu.PopupOuterWidth, outerH = menu.PopupOuterHeight;
float inX = RetailChromeSprites.Border, inY = outerTop + RetailChromeSprites.Border;
Assert.True(HasFillQuad(segs, 0f, outerTop, outerW, outerH, menu.PlainBackgroundColor),
"expected the plain popup background fill");
Assert.True(HasFillQuad(segs, inX, inY + 1 * PlainRowHeight, PlainColumnWidth, PlainRowHeight, menu.PlainSelectedColor),
"expected row 1 (selected/current) filled with PlainSelectedColor");
Assert.True(HasFillQuad(segs, inX, inY + 2 * PlainRowHeight, PlainColumnWidth, PlainRowHeight, menu.PlainHoverColor),
"expected row 2 (hovered) filled with PlainHoverColor");
// background(1) + outline(4 sides) + selected row(1) + hovered row(1) = 7,
// nothing else untextured.
Assert.Equal(7, QuadCount(segs, 0u));
}
[Fact]
public void Plain_OpenPopup_RowText_LeftAlignedAtPlainPadding()
{
var menu = MakePopupMenu(retailButtonArt: false, itemCount: 1, rowsPerColumn: 7, scrollable: false);
OpenAndHover(menu);
var (renderer, ctx) = MakeContext(200f, 200f);
menu.DrawOverlays(ctx);
// Item 0's label is "W" — the one glyph MakeFont() defines — so exactly
// one FontTexture quad renders, at column 0's PlainPadding inset (no
// authored TextIndent/centering in plain mode).
var glyphSeg = Assert.Single(renderer.DebugSpriteSegmentVerts, s => s.Texture == FontTexture);
Assert.Equal(RetailChromeSprites.Border + UiMenu.PlainPadding, glyphSeg.Verts[0], 3);
}
[Fact]
public void Plain_OpenPopup_ScrollableOverflow_DrawsPlainTrackAndFlatThumb_NoDatArt()
{
int resolveCalls = 0;
var menu = MakePopupMenu(retailButtonArt: false, itemCount: 12, rowsPerColumn: 5, scrollable: true,
countResolveCall: n => resolveCalls += n);
menu.Selected = 0; // row 0 (visible) is "current"
OpenAndHover(menu);
var (renderer, ctx) = MakeContext(200f, 200f);
menu.DrawOverlays(ctx);
var segs = renderer.DebugSpriteSegmentVerts;
Assert.True(menu.PopupScroll.HasOverflow);
Assert.Equal(0, resolveCalls);
Assert.Equal(0, QuadCount(segs, menu.ScrollTrackSprite));
Assert.Equal(0, QuadCount(segs, menu.ScrollThumbSprite));
float outerTop = menu.Height;
float inX = RetailChromeSprites.Border, inY = outerTop + RetailChromeSprites.Border;
float scrollbarX = inX + PlainColumnWidth;
Assert.True(HasFillQuad(segs, inX, inY, PlainColumnWidth, PlainRowHeight, menu.PlainSelectedColor),
"expected visible row 0 (selected/current) filled with PlainSelectedColor");
Assert.True(HasFillQuad(segs, scrollbarX, inY, menu.ScrollbarWidth, 5 * PlainRowHeight, menu.PlainBackgroundColor),
"expected the scrollbar track background fill");
// popup bg(1)+outline(4) + selected row(1) + scrollbar bg(1)+outline(4) + thumb(1) = 12.
Assert.Equal(12, QuadCount(segs, 0u));
}
[Fact]
public void Plain_ScrollablePopup_ContentFits_DrawsTrackWithNoThumb()
{
var menu = MakePopupMenu(retailButtonArt: false, itemCount: 3, rowsPerColumn: 5, scrollable: true);
OpenAndHover(menu);
var (renderer, ctx) = MakeContext(200f, 200f);
menu.DrawOverlays(ctx);
var segs = renderer.DebugSpriteSegmentVerts;
Assert.False(menu.PopupScroll.HasOverflow);
// popup bg(1)+outline(4) + scrollbar bg(1)+outline(4) = 10, no thumb quad
// (nothing selected/hovered here either).
Assert.Equal(10, QuadCount(segs, 0u));
}
[Fact]
public void Plain_OpenPopup_HitTesting_SelectsHoveredRow_ClosesPopup()
{
// The new hover-tracking MouseMove handling must not change what a
// MouseDown on the same row does — same rows, same scroll, same pick.
object? picked = null;
var menu = MakePopupMenu(retailButtonArt: false, itemCount: 3, rowsPerColumn: 7, scrollable: false);
menu.OnSelect = p => picked = p;
OpenAndHover(menu, hoverRow: 2);
int ly = 20 + RetailChromeSprites.Border + 2 * (int)PlainRowHeight + (int)(PlainRowHeight / 2);
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, Data1: 10, Data2: ly)));
Assert.Equal(2, picked);
Assert.False(menu.IsOpen);
}
[Fact]
public void Retail_OpenPopup_DrawIsByteForByteUnchanged_RegressionGolden()
{
// A golden pin for the OPEN popup on a retail-styled (RetailButtonArt=true,
// the class default) menu — proves the S7-follow-up refactor of
// OnDrawOverlay (adding the plain branch) left the retail branch
// byte-identical: same bevel, same panel-fill sprite, same per-row
// highlight/normal sprite, and critically NO untextured fill anywhere
// (the plain path is a fully separate branch, never blended in).
var menu = MakePopupMenu(retailButtonArt: true, itemCount: 2, rowsPerColumn: 7, scrollable: false);
menu.Selected = 1;
OpenAndHover(menu);
var (renderer, ctx) = MakeContext(200f, 200f);
menu.DrawOverlays(ctx);
var segs = renderer.DebugSpriteSegmentVerts;
Assert.Equal(1, QuadCount(segs, RetailChromeSprites.CenterFill)); // bevel drawn
Assert.Equal(1, QuadCount(segs, 0x0600124Cu)); // PopupBgSprite panel fill
Assert.Equal(1, QuadCount(segs, 0x0600124Du)); // ItemHighlightSprite (row 1, selected)
Assert.Equal(1, QuadCount(segs, 0x0600124Eu)); // ItemNormalSprite (row 0)
Assert.Equal(0, QuadCount(segs, 0u)); // no untextured fill in the retail path
}
}