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:
commit
22e6ae88a1
3 changed files with 472 additions and 2 deletions
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue