fix #385: Options dropdowns — white centered text + size-to-content popup
User gate report (Campaign OP happy-testing round, 2026-08-13): every Config-tab dropdown drew its text gold + left-aligned and its popup a fixed 6 rows regardless of item count. All three were unmeasured styling divergences — the authored data (new probe menuprobe3, live DAT) says: - button label child 0x10000355: fontColor white, hJustify=Center - row template 0x1000035A: fontColor white, hJustify=Center - popup ListBox 0x10000358: edge-docked L=T=R=B=1, the authored condition arming retail UIElement_Menu::RecalculatePopupSize @0x0046caf0 — popup resizes to the ListBox's summed content height, uncapped (0x0046e5f4..0046e66c via ResizeScrollableArea's 0x32 broadcast) UiMenu gains three opt-in properties (ButtonTextCentered, ItemTextCentered, PopupSizeToContent) plus retail Open @0x0046cc42's empty-list gate; chat + vendor keep the class defaults, so their shipped behavior is untouched. ConfigOptionsPageController.ApplyMenuChrome wires all four corrections for the 8 Config menus with the probe citation. The same probe found vendor's authored popup ListBox is ALSO docked while our vendor dropdown ships G5's fixed 6-row window — filed as #386 + register row AD-88 (UNCLEAR: the G5 retail screenshot and the decomp mechanism conflict) instead of silently reworking a user-gated surface. The "resolution change resizes the window" observation from the same report is #374's designed windowed-mode behavior (display-mode switching is #376/#377) — no change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
028920420d
commit
a9b6435f55
8 changed files with 422 additions and 9 deletions
|
|
@ -314,6 +314,21 @@ public static class ConfigOptionsPageController
|
|||
// glyph to clear space for (see MenuChromeSprites' own doc).
|
||||
menu.TextIndent = 0f;
|
||||
menu.ButtonTextIndent = 0f;
|
||||
|
||||
// User gate report 2026-08-13 (happy-testing round): dropdown text
|
||||
// drew gold + left-aligned and the popup a fixed 6 rows. All four
|
||||
// corrections below are MEASURED authored facts (menuprobe3,
|
||||
// OptionsPanelLiveMountProbeTests.ProbeMenuPopupSizingAndTextStyle),
|
||||
// not styling choices:
|
||||
// - button label child 0x10000355: fontColor white, hJustify=Center;
|
||||
// - row template 0x1000035A: fontColor white, hJustify=Center;
|
||||
// - popup ListBox 0x10000358: edges L=T=R=B=1 (docked), which arms
|
||||
// retail's RecalculatePopupSize @0x0046caf0 size-to-content path
|
||||
// (see UiMenu.PopupSizeToContent's own doc for the mechanism).
|
||||
menu.TextColor = Vector4.One;
|
||||
menu.ButtonTextCentered = true;
|
||||
menu.ItemTextCentered = true;
|
||||
menu.PopupSizeToContent = true;
|
||||
}
|
||||
|
||||
/// <summary>The live read/write seam every row on this page writes/reads
|
||||
|
|
|
|||
|
|
@ -190,6 +190,47 @@ public sealed class UiMenu : UiElement
|
|||
public uint ItemNormalSprite { get; set; } // 0x0600124E — a row background (191×17)
|
||||
public uint ItemHighlightSprite { get; set; } // 0x0600124D — the active channel's row
|
||||
|
||||
/// <summary>
|
||||
/// Port of retail <c>UIElement_Menu::RecalculatePopupSize @0x0046caf0</c>
|
||||
/// (user gate report 2026-08-13: the Sound Features popup drew a fixed 6
|
||||
/// rows tall for a 3-item list): when the authored popup ListBox is
|
||||
/// edge-docked on both top+bottom (<c>m_topEdge==1 &&
|
||||
/// m_bottomEdge==1</c>), retail resizes the popup to the ListBox's
|
||||
/// scrollable CONTENT extent — the SUM of laid-out row heights, uncapped
|
||||
/// (<c>0x0046e5f4..0046e66c</c> feeding
|
||||
/// <c>UIElement_Scrollable::ResizeScrollableArea @0x00474730</c>, whose
|
||||
/// 0x32 broadcast RecalculatePopupSize answers) — so the popup shrinks
|
||||
/// AND grows to the item count. Whether the path is active is an
|
||||
/// authored per-menu fact, not a convention: the Config option-menu
|
||||
/// popup ListBox (0x21000043/0x10000358) reads edges L=T=R=B=1
|
||||
/// (menuprobe3, <c>OptionsPanelLiveMountProbeTests</c>), so
|
||||
/// <see cref="AcDream.App.UI.Layout.ConfigOptionsPageController"/> sets
|
||||
/// this true; chat's grid popup and vendor's shipped 6-row window keep
|
||||
/// the class default false (vendor's authored ListBox is ALSO docked —
|
||||
/// tracked as its own issue, not silently reworked here).
|
||||
/// When set, <see cref="RowsPerColumn"/> stops being the visible-window
|
||||
/// height and the popup shows every item with no scroll overflow.
|
||||
/// </summary>
|
||||
public bool PopupSizeToContent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Retail draws the button caption through the authored label child named
|
||||
/// by menu attr 8 (<c>UIElement_Menu::NewSelection @0x0046cd60</c> writes
|
||||
/// the selected item's text into it) — its justification comes from the
|
||||
/// LayoutDesc, not menu code. The Config option-menu label child
|
||||
/// (0x21000043/0x10000355) authors hJustify=Center (menuprobe3);
|
||||
/// chat's own label child authors Left, the class default.
|
||||
/// </summary>
|
||||
public bool ButtonTextCentered { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Same authored-justification rule for the popup rows: each row is an
|
||||
/// authored text template (menu attr 9). The Config option-menu row
|
||||
/// template (0x21000043/0x1000035A) authors hJustify=Center
|
||||
/// (menuprobe3); chat's and vendor's row templates author Left.
|
||||
/// </summary>
|
||||
public bool ItemTextCentered { get; set; }
|
||||
|
||||
public Vector4 TextColor { get; set; } = new(1f, 0.92f, 0.72f, 1f);
|
||||
/// <summary>Available item text — retail white #FFFFFF (gmMainChatUI talk-focus
|
||||
/// enabled state). Confirmed via decomp: enabled items render white.</summary>
|
||||
|
|
@ -232,10 +273,28 @@ public sealed class UiMenu : UiElement
|
|||
private float InteriorW => Scrollable
|
||||
? ColumnWidth + ScrollbarWidth
|
||||
: ColumnCount * ColumnWidth;
|
||||
private float InteriorH => RowsPerColumn * RowHeight;
|
||||
|
||||
/// <summary>The popup's visible row count. Size-to-content (retail's
|
||||
/// RecalculatePopupSize path — see <see cref="PopupSizeToContent"/>):
|
||||
/// every item, uncapped; otherwise the authored fixed window
|
||||
/// (<see cref="RowsPerColumn"/>). Max(1,·) keeps a detached/empty test
|
||||
/// menu's geometry finite — a live empty menu never opens (retail
|
||||
/// <c>Open @0x0046cc42</c> gates on <c>m_listItems.m_num != 0</c>,
|
||||
/// ported in <see cref="OnEvent"/>).</summary>
|
||||
private int EffectiveVisibleRows => Scrollable && PopupSizeToContent
|
||||
? System.Math.Max(1, Items.Count)
|
||||
: RowsPerColumn;
|
||||
|
||||
private float InteriorH => EffectiveVisibleRows * RowHeight;
|
||||
private float OuterW => InteriorW + 2 * Border;
|
||||
private float OuterH => InteriorH + 2 * Border;
|
||||
|
||||
/// <summary>The open popup's outer (bevel-inclusive) height — read-only
|
||||
/// test seam, same rationale as <see cref="PopupScroll"/>/<see cref="IsOpen"/>:
|
||||
/// the size-to-content geometry has no other assertable surface short of
|
||||
/// a full render pass.</summary>
|
||||
public float PopupOuterHeight => OuterH;
|
||||
|
||||
/// <summary>
|
||||
/// G7 (vendor gate finding, item 2 — popup direction): port of retail
|
||||
/// <c>UIElement_Menu::Open</c> (pc:120210-120252, <c>0x0046cc30</c>)'s Y placement:
|
||||
|
|
@ -282,7 +341,15 @@ public sealed class UiMenu : UiElement
|
|||
var (tex, tw, _) = resolve(_open ? PressedSprite : NormalSprite);
|
||||
if (tex != 0 && tw > 0) DrawButtonFace(ctx, tex, tw);
|
||||
}
|
||||
DrawLabel(ctx, ButtonLabelProvider?.Invoke() ?? "", ButtonTextIndent, (Height - LineH()) * 0.5f, TextColor);
|
||||
string caption = ButtonLabelProvider?.Invoke() ?? "";
|
||||
// Centered captions centre within the label-child band — the authored
|
||||
// label child spans the button MINUS the arrow-cap overlay's right
|
||||
// socket (0x10000355 is 100 wide of the 117 button, docked; the
|
||||
// arrow child overlays the last 17px — menuprobe3).
|
||||
float capX = ButtonTextCentered
|
||||
? MathF.Max(0f, (Width - (ArrowCapClosedSprite != 0 ? ArrowCapWidth : 0f) - MeasureText(caption)) * 0.5f)
|
||||
: ButtonTextIndent;
|
||||
DrawLabel(ctx, caption, capX, (Height - LineH()) * 0.5f, TextColor);
|
||||
|
||||
// G6: the open/closed arrow-cap overlay — see ArrowCapClosedSprite's doc comment.
|
||||
if (resolve is not null) DrawArrowCap(ctx, resolve);
|
||||
|
|
@ -373,11 +440,21 @@ public sealed class UiMenu : UiElement
|
|||
int col = i / RowsPerColumn, row = i % RowsPerColumn;
|
||||
// Items grey out when unavailable; when EnabledProvider is null all items are enabled.
|
||||
bool avail = EnabledProvider?.Invoke(Items[i].Payload) ?? true;
|
||||
DrawLabel(ctx, Items[i].Label, inX + col * ColumnWidth + TextIndent, inY + row * RowHeight + textY,
|
||||
DrawLabel(ctx, Items[i].Label, inX + col * ColumnWidth + ItemTextX(Items[i].Label),
|
||||
inY + row * RowHeight + textY,
|
||||
avail ? TextColorAvailable : TextColorGhosted);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>A row label's X offset within its column — the authored row
|
||||
/// template's own justification (see <see cref="ItemTextCentered"/>).</summary>
|
||||
private float ItemTextX(string label) => ItemTextCentered
|
||||
? MathF.Max(0f, (ColumnWidth - MeasureText(label)) * 0.5f)
|
||||
: TextIndent;
|
||||
|
||||
private float MeasureText(string s)
|
||||
=> DatFont?.MeasureWidth(s) ?? Font?.MeasureWidth(s) ?? s.Length * 7f;
|
||||
|
||||
/// <summary>
|
||||
/// G5: single-column popup with a docked scrollbar — port of the vendor category
|
||||
/// dropdown's authored shape (LayoutDesc <c>0x21000043</c>, see <see cref="Scrollable"/>'s
|
||||
|
|
@ -397,7 +474,7 @@ public sealed class UiMenu : UiElement
|
|||
DrawSprite(ctx, resolve, PopupBgSprite, inX, inY, ColumnWidth, InteriorH);
|
||||
|
||||
int start = VisibleTopRow;
|
||||
int count = System.Math.Min(RowsPerColumn, Items.Count - start);
|
||||
int count = System.Math.Min(EffectiveVisibleRows, Items.Count - start);
|
||||
float textY = (RowHeight - LineH()) * 0.5f;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
|
|
@ -411,7 +488,7 @@ public sealed class UiMenu : UiElement
|
|||
int idx = start + i;
|
||||
float y = inY + i * RowHeight;
|
||||
bool avail = EnabledProvider?.Invoke(Items[idx].Payload) ?? true;
|
||||
DrawLabel(ctx, Items[idx].Label, inX + TextIndent, y + textY,
|
||||
DrawLabel(ctx, Items[idx].Label, inX + ItemTextX(Items[idx].Label), y + textY,
|
||||
avail ? TextColorAvailable : TextColorGhosted);
|
||||
}
|
||||
|
||||
|
|
@ -425,7 +502,10 @@ public sealed class UiMenu : UiElement
|
|||
{
|
||||
int lineHeight = System.Math.Max(1, (int)MathF.Round(RowHeight));
|
||||
PopupScroll.LineHeight = lineHeight;
|
||||
PopupScroll.SetExtents(Items.Count * lineHeight, RowsPerColumn * lineHeight);
|
||||
// Size-to-content: view == content, so HasOverflow is false and the
|
||||
// scrollbar draws its chrome with no thumb (retail's authored
|
||||
// scrollbar sibling stretches with the docked popup the same way).
|
||||
PopupScroll.SetExtents(Items.Count * lineHeight, EffectiveVisibleRows * lineHeight);
|
||||
}
|
||||
|
||||
/// <summary>Index of the first visible row — nearest-row snap of the (possibly
|
||||
|
|
@ -436,7 +516,7 @@ public sealed class UiMenu : UiElement
|
|||
get
|
||||
{
|
||||
int lineHeight = System.Math.Max(1, (int)MathF.Round(RowHeight));
|
||||
int maxStart = System.Math.Max(0, Items.Count - RowsPerColumn);
|
||||
int maxStart = System.Math.Max(0, Items.Count - EffectiveVisibleRows);
|
||||
int row = (int)MathF.Round((float)PopupScroll.ScrollY / lineHeight);
|
||||
return System.Math.Clamp(row, 0, maxStart);
|
||||
}
|
||||
|
|
@ -581,6 +661,10 @@ public sealed class UiMenu : UiElement
|
|||
return true;
|
||||
}
|
||||
|
||||
// Retail Open @0x0046cc42 refuses an empty list (gates on
|
||||
// m_listBox->m_listItems.m_num != 0) — a bare click on an itemless
|
||||
// menu is a no-op rather than an empty popup.
|
||||
if (!_open && Items.Count == 0) return true;
|
||||
SetOpen(!_open); // toggle on button click
|
||||
return true;
|
||||
}
|
||||
|
|
@ -598,7 +682,7 @@ public sealed class UiMenu : UiElement
|
|||
{
|
||||
int row = (int)(iy / RowHeight);
|
||||
int idx = VisibleTopRow + row;
|
||||
if (row >= 0 && row < RowsPerColumn && idx >= 0 && idx < Items.Count
|
||||
if (row >= 0 && row < EffectiveVisibleRows && idx >= 0 && idx < Items.Count
|
||||
&& (EnabledProvider?.Invoke(Items[idx].Payload) ?? true))
|
||||
{
|
||||
OnSelect?.Invoke(Items[idx].Payload);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue