diff --git a/src/AcDream.App/UI/UiMenu.cs b/src/AcDream.App/UI/UiMenu.cs
index 810886a0..95a03f66 100644
--- a/src/AcDream.App/UI/UiMenu.cs
+++ b/src/AcDream.App/UI/UiMenu.cs
@@ -160,6 +160,22 @@ public sealed class UiMenu : UiElement
private bool _draggingPopupThumb;
private float _popupThumbDragOffset;
+ /// Index into of the row under the pointer while
+ /// the plain popup is open, or -1. Presentation-only (see
+ /// 's doc) — retail's sprite popup has no
+ /// equivalent hover concept, so this never affects the retail draw path.
+ private int _hoveredPopupIndex = -1;
+
+ /// Test seam, same rationale as .
+ internal int HoveredPopupIndexForTest => _hoveredPopupIndex;
+
+ ///
+ /// 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 is false).
+ ///
+ 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.
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.
+ /// The current entry's row fill — identical value to
+ /// so a plugin's open dropdown
+ /// reads as the same widget family as its lists.
+ public Vector4 PlainSelectedColor { get; set; } = new(0.28f, 0.23f, 0.08f, 0.95f);
+ /// A slightly lighter fill for the row under the pointer (no
+ /// separate glyph or sprite swap — fills only, mirroring
+ /// 's "tint, never a sprite swap" rule
+ /// for the closed state).
+ public Vector4 PlainHoverColor { get; set; } = new(0.40f, 0.33f, 0.14f, 0.95f);
+
private bool _open;
///
@@ -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.
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.
+
+ /// Plain counterpart of — flat fill +
+ /// 1px outline instead of the bevel/panel sprites, per-row selected/hover
+ /// fills instead of highlight sprites, /
+ /// labels left-aligned at
+ /// instead of the authored /
+ /// 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).
+ 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);
+ }
+ }
+
+ /// Plain counterpart of — same
+ /// -sliced single column, plain
+ /// selected/hover row fills, and a plain scrollbar
+ /// () instead of the sprite chrome.
+ 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);
+ }
+
+ ///
+ /// Plain counterpart of : a 1px-bordered
+ /// track and a flat thumb, both in — no DAT
+ /// thumb/track/arrow-button art at all. Shares the exact same
+ /// geometry (so the thumb's drawn
+ /// position matches '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
+ /// ( is unchanged).
+ ///
+ 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);
+ }
+
+ ///
+ /// Recomputes the hovered popup row from a MouseMove's local (lx,ly) —
+ /// same convention 's MouseDown handling already uses
+ /// (/-relative). Plain-mode-only:
+ /// see 's doc comment for why this is
+ /// never invoked on the retail sprite-popup path.
+ ///
+ 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;
+ }
+
/// Draw the universal 8-piece retail window bevel (corners + tiled edges +
/// tiled centre fill) framing the rect (,,
/// ,). 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;
}
diff --git a/tests/AcDream.App.Tests/UI/UiMenuPlainStyleTests.cs b/tests/AcDream.App.Tests/UI/UiMenuPlainStyleTests.cs
index 013181e6..66c5bf68 100644
--- a/tests/AcDream.App.Tests/UI/UiMenuPlainStyleTests.cs
+++ b/tests/AcDream.App.Tests/UI/UiMenuPlainStyleTests.cs
@@ -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? 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 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;
+ }
+
+ /// Opens the popup (MouseDown on the closed face) then, if given,
+ /// hovers a row via MouseMove — the same (Data1,Data2) local-coordinate
+ /// convention already uses for MouseDown.
+ 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
+ }
}