fix(vt): plain-style menu popup — no retail gradient/checkmark/gold scrollbar
Owner live-client report 2026-09-07 ("Drop down menus look horrible, there
is also a checkmark on the text there"): the prior S7 fix only replaced the
CLOSED-state button face when RetailButtonArt=false. The OPEN popup still
drew retail's sprite art unconditionally — the tan/orange gradient panel
(PopupBgSprite), the per-row highlight sprite (which bakes a checkbox/
checkmark glyph into its leftmost ~17px, per TextIndent's doc comment), and
the ornate gold scrollbar chrome — regardless of the menu's style.
UiMenu.OnDrawOverlay now branches on RetailButtonArt before ever reading
SpriteResolve: plain mode draws through two new methods (DrawGridPopupPlain,
DrawScrollablePopupPlain) that use only DrawFill/DrawRectOutline — flat
background + 1px border, the current entry filled like a list selection
(PlainSelectedColor, same value as UiMarkupList.SelectedColor), a new
hover fill (PlainHoverColor) for the row under the pointer, and left-aligned
text at PlainPadding. No checkmark is possible by construction since plain
mode never resolves or draws any sprite. Hover tracking needed a small new
mechanism: UiMenu.ReceivesHoverMouseMove now returns true while a plain
popup is open, so UiRoot's hover dispatch keeps delivering MouseMove to
_hoveredPopupIndex (reset on every open/close transition and on
HoverLeave). Scrollbar overflow (DrawPopupScrollbarPlain) draws a 1px-
bordered track and a flat thumb, both in PlainBorderColor, sharing the
exact UiScrollbar.ThumbRect geometry the hit-test math already uses — no
DAT track/thumb/arrow-button art. Hit-testing (OnHitTest/OnEvent's
MouseDown pick logic) is untouched; the retail sprite branch is now a
separate, unmodified path proven byte-identical by a new golden test.
Mutation proof: reverting UiMenu.cs alone (keeping the new tests) fails the
build outright — the six new tests reference PlainSelectedColor/
PlainHoverColor, which only exist after this change (CS1061 with the old
class). Filters run: AcDream.App.Tests Markup|UiMenu|Menu|Scrollbar
(242 passed, 3 pre-existing unrelated Lane=Manual live-DAT-probe failures
that require ACDREAM_PROBE_LIVE_MOUNT=1 and predate this change) and
AcDream.Plugins.MossTank.Tests Markup (9/9 passed).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
fab134ae4f
commit
5bdf2fa395
2 changed files with 460 additions and 1 deletions
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue