fix #378: Config-tab dropdown menus render bare with no popup chrome
Root cause: DatWidgetFactory builds Config-tab Type-0x10000038 menu leaves as bare UiMenu instances (matching the vendor/chat channel menu pattern), but unlike those two controllers, ConfigOptionsPageController never wired the menu's sprite/font/geometry properties after Bind — so every dropdown rendered as plain text with no button well, no arrow cap, and opened no popup on click (#374's fix only corrected click ROUTING, not the missing chrome). Fix: ConfigOptionsPageController.ApplyMenuChrome wires every Config-tab menu row with the SAME retail sprite ids VendorUiController/ ChatWindowController's channel menu already use for this shared popup catalog (LayoutDesc 0x21000043), verified against the live DAT via OptionsPanelLiveMountProbeTests' ProbeConfigMenuChrome/ ProbeConfigMenuPopupChrome probes. Regressed by ConfigOptionsPageControllerTests.MenuRow_SoundFeatures_OpensAndSelects ThroughRealHitPath_UsingAuthoredPopupGeometry, which drives the real click-to-open + item-pick path through the authored popup geometry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
28bef4e03b
commit
c121842664
6 changed files with 524 additions and 35 deletions
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.UI.Abstractions.Panels.Settings;
|
||||
|
||||
|
|
@ -217,6 +218,104 @@ public static class ConfigOptionsPageController
|
|||
/// <c>0x10000219</c> convention.</summary>
|
||||
private const uint ToggleCheckboxElementId = 0x10000219u;
|
||||
|
||||
/// <summary>
|
||||
/// #378 fix (2026-08-11, gate 4): every Config-tab dropdown
|
||||
/// (<see cref="MenuElementId"/>, retail class <c>UIOption_Menu</c>,
|
||||
/// <c>Type 0x10000038</c>) rendered bare — no button well, no arrow, no
|
||||
/// popup — because NOTHING wired its sprites, the same class of bug
|
||||
/// <see cref="VendorUiController"/>'s own precedent already fixed for
|
||||
/// the vendor category dropdown. A live-DAT probe (menuprobe2,
|
||||
/// <c>OptionsPanelLiveMountProbeTests</c>) walked the retail inheritance
|
||||
/// chain the built widget tree collapses away
|
||||
/// (<c>0x10000224</c> → base <c>0x10000353</c> in LayoutDesc
|
||||
/// <c>0x21000043</c> — retail's SHARED popup/dropdown catalog, the exact
|
||||
/// same layout <see cref="VendorUiController"/>'s own <c>0x100000BF</c>
|
||||
/// also derives from, base <c>0x1000034B</c>) and found the two
|
||||
/// prototypes BYTE-IDENTICAL in every sprite id: arrow cap
|
||||
/// <c>0x060012B1</c>/<c>B2</c>, face/row sprite <c>0x060012B3</c>/<c>B4</c>,
|
||||
/// and the full 6-sprite scrollbar chrome
|
||||
/// <c>0x06004C5F/60/63/66/69/6C</c> — Config's own popup root/ListBox
|
||||
/// (<c>0x10000357</c>/<c>0x10000358</c>, attrs 6/2 on <c>0x10000353</c>)
|
||||
/// carry the SAME <c>0x59</c>=true/<c>0x5D</c>=1-col/<c>0x5E</c>=6-row/
|
||||
/// <c>0x61</c>=true properties as vendor's own <c>0x1000034F</c>/
|
||||
/// <c>0x10000350</c>, and Config's row template (attr 9 →
|
||||
/// <c>0x1000035A</c>) resolves the IDENTICAL Normal/Highlight media as
|
||||
/// vendor's own <c>0x10000352</c>. Attribute 7 (the popup catalog
|
||||
/// LayoutDesc) is <c>0x21000043</c> for BOTH — not an approximation
|
||||
/// choice, a measured fact; no register row needed. No property "5"
|
||||
/// (open-upward) is authored on <c>0x10000353</c> either, matching
|
||||
/// vendor's own absent-defaults-false case — the popup opens DOWNWARD.
|
||||
/// These constants are therefore transcribed from the SAME live values
|
||||
/// <see cref="VendorUiController"/> already hardcodes, not duplicated by
|
||||
/// coincidence — a genuine shared retail asset, verified independently
|
||||
/// rather than assumed from the precedent.
|
||||
/// </summary>
|
||||
private static class MenuChromeSprites
|
||||
{
|
||||
public const uint Normal = 0x060012B3u;
|
||||
public const uint Pressed = 0x060012B4u;
|
||||
public const uint ItemNormal = 0x060012B3u;
|
||||
public const uint ItemHighlight = 0x060012B4u;
|
||||
public const uint ArrowCapClosed = 0x060012B1u;
|
||||
public const uint ArrowCapOpen = 0x060012B2u;
|
||||
|
||||
public const int RowsPerColumn = 6;
|
||||
public const float RowHeight = 18f;
|
||||
public const float ColumnWidth = 100f;
|
||||
|
||||
public const float ScrollbarWidth = 16f;
|
||||
public const float ScrollButtonExtent = 16f;
|
||||
public const uint ScrollTrack = 0x06004C5Fu;
|
||||
public const uint ScrollThumbTop = 0x06004C60u;
|
||||
public const uint ScrollThumb = 0x06004C63u;
|
||||
public const uint ScrollThumbBottom = 0x06004C66u;
|
||||
public const uint ScrollUp = 0x06004C69u;
|
||||
public const uint ScrollDown = 0x06004C6Cu;
|
||||
}
|
||||
|
||||
/// <summary>Applies <see cref="MenuChromeSprites"/> + geometry to a
|
||||
/// freshly-built <see cref="UiMenu"/> leaf — the SAME wiring shape
|
||||
/// <see cref="VendorUiController"/> performs inline for its own dropdown
|
||||
/// (font/sprite resolvers, scrollable single-column popup, arrow-cap
|
||||
/// overlay, zero text indents since neither the row template nor the
|
||||
/// face carry a checkbox/LED glyph to clear — see the class's own
|
||||
/// checked live-dat properties).</summary>
|
||||
private static void ApplyMenuChrome(
|
||||
UiMenu menu,
|
||||
Func<uint, (uint tex, int w, int h)>? resolveSprite,
|
||||
UiDatFont? datFont,
|
||||
BitmapFont? debugFont)
|
||||
{
|
||||
menu.SpriteResolve = resolveSprite;
|
||||
menu.DatFont = datFont;
|
||||
menu.Font = debugFont;
|
||||
menu.NormalSprite = MenuChromeSprites.Normal;
|
||||
menu.PressedSprite = MenuChromeSprites.Pressed;
|
||||
menu.ItemNormalSprite = MenuChromeSprites.ItemNormal;
|
||||
menu.ItemHighlightSprite = MenuChromeSprites.ItemHighlight;
|
||||
menu.RowsPerColumn = MenuChromeSprites.RowsPerColumn;
|
||||
menu.RowHeight = MenuChromeSprites.RowHeight;
|
||||
menu.ColumnWidth = MenuChromeSprites.ColumnWidth;
|
||||
menu.Scrollable = true;
|
||||
menu.ScrollbarWidth = MenuChromeSprites.ScrollbarWidth;
|
||||
menu.ScrollButtonExtent = MenuChromeSprites.ScrollButtonExtent;
|
||||
menu.ScrollTrackSprite = MenuChromeSprites.ScrollTrack;
|
||||
menu.ScrollThumbTopSprite = MenuChromeSprites.ScrollThumbTop;
|
||||
menu.ScrollThumbSprite = MenuChromeSprites.ScrollThumb;
|
||||
menu.ScrollThumbBottomSprite = MenuChromeSprites.ScrollThumbBottom;
|
||||
menu.ScrollUpSprite = MenuChromeSprites.ScrollUp;
|
||||
menu.ScrollDownSprite = MenuChromeSprites.ScrollDown;
|
||||
menu.ArrowCapClosedSprite = MenuChromeSprites.ArrowCapClosed;
|
||||
menu.ArrowCapOpenSprite = MenuChromeSprites.ArrowCapOpen;
|
||||
// No property "5" authored on the retail prototype — absent-defaults-
|
||||
// false, opens DOWNWARD (see MenuChromeSprites' own doc).
|
||||
menu.OpenUpward = false;
|
||||
// Neither the row template nor the face child carry a checkbox/LED
|
||||
// glyph to clear space for (see MenuChromeSprites' own doc).
|
||||
menu.TextIndent = 0f;
|
||||
menu.ButtonTextIndent = 0f;
|
||||
}
|
||||
|
||||
/// <summary>The live read/write seam every row on this page writes/reads
|
||||
/// through — four settings groups, each read once per row-build and
|
||||
/// mutated read-modify-write per change (the SAME per-change persistence
|
||||
|
|
@ -237,12 +336,26 @@ public static class ConfigOptionsPageController
|
|||
/// ListBox, links its scrollbar, seeds every row's current/default
|
||||
/// state, and registers each row into <paramref name="page"/>.
|
||||
/// </summary>
|
||||
/// <param name="resolveSprite">#378: RenderSurface id → (GL tex handle,
|
||||
/// pixel width, pixel height), threaded onto every dropdown's
|
||||
/// <see cref="UiMenu.SpriteResolve"/> — see <see cref="MenuChromeSprites"/>'s
|
||||
/// own doc. Null (fixture/conformance callers that don't exercise
|
||||
/// dropdown chrome) leaves every menu's sprite ids populated but drawing
|
||||
/// nothing, matching <see cref="UiMenu"/>'s own null-safety.</param>
|
||||
/// <param name="datFont">#378: the retail dat-driven font every dropdown
|
||||
/// draws its button/row labels with.</param>
|
||||
/// <param name="debugFont">#378: fallback bitmap font when
|
||||
/// <paramref name="datFont"/> is unavailable — same convention
|
||||
/// <see cref="VendorUiController"/> uses.</param>
|
||||
public static bool Bind(
|
||||
ImportedLayout layout,
|
||||
OptionPage page,
|
||||
Func<uint, uint, UiElement?> templateResolver,
|
||||
Func<uint, uint, string?> resolveString,
|
||||
Bindings bindings)
|
||||
Bindings bindings,
|
||||
Func<uint, (uint tex, int w, int h)>? resolveSprite = null,
|
||||
UiDatFont? datFont = null,
|
||||
BitmapFont? debugFont = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(layout);
|
||||
ArgumentNullException.ThrowIfNull(page);
|
||||
|
|
@ -293,17 +406,17 @@ public static class ConfigOptionsPageController
|
|||
CameraTurningSettings cameraTurning = bindings.LoadCameraTurning();
|
||||
ChatSettings chat = bindings.LoadChat();
|
||||
|
||||
BindSoundSection(listBox, page, resolveString, bindings, ref audio);
|
||||
BindSoundSection(listBox, page, resolveString, bindings, ref audio, resolveSprite, datFont, debugFont);
|
||||
BuildSeparatorRow(listBox);
|
||||
BindCameraSection(listBox, page, resolveString, bindings, ref cameraTurning);
|
||||
BuildSeparatorRow(listBox);
|
||||
BindGraphicsSection(listBox, page, resolveString, bindings, ref display);
|
||||
BindGraphicsSection(listBox, page, resolveString, bindings, ref display, resolveSprite, datFont, debugFont);
|
||||
BuildSeparatorRow(listBox);
|
||||
BindRenderingQualitySection(listBox, page, resolveString, bindings, ref display);
|
||||
BindRenderingQualitySection(listBox, page, resolveString, bindings, ref display, resolveSprite, datFont, debugFont);
|
||||
BuildSeparatorRow(listBox);
|
||||
BindInputSection(listBox, page, resolveString, bindings, ref cameraTurning);
|
||||
BuildSeparatorRow(listBox);
|
||||
BindUiSection(listBox, page, resolveString, bindings, ref chat);
|
||||
BindUiSection(listBox, page, resolveString, bindings, ref chat, resolveSprite, datFont, debugFont);
|
||||
// OP6 rework (2026-08-11, review S1): retail's own InitOptions ends
|
||||
// with a SIXTH AddSeperator tailcall (0x0049e80d) — a trailing
|
||||
// separator after the LAST section, not just the five INTERIOR ones
|
||||
|
|
@ -322,7 +435,10 @@ public static class ConfigOptionsPageController
|
|||
OptionPage page,
|
||||
Func<uint, uint, string?> resolveString,
|
||||
Bindings bindings,
|
||||
ref AudioSettings audio)
|
||||
ref AudioSettings audio,
|
||||
Func<uint, (uint tex, int w, int h)>? resolveSprite,
|
||||
UiDatFont? datFont,
|
||||
BitmapFont? debugFont)
|
||||
{
|
||||
BuildHeaderRow(listBox, "ID_Sound_SoundSection", resolveString);
|
||||
|
||||
|
|
@ -337,7 +453,8 @@ public static class ConfigOptionsPageController
|
|||
bindings.SaveAudio(updated);
|
||||
},
|
||||
defaultValue: 0,
|
||||
storeOnly: true); // AP-199
|
||||
storeOnly: true, // AP-199
|
||||
resolveSprite, datFont, debugFont);
|
||||
|
||||
// OP6 rework (2026-08-11, review M2): read/apply the ENABLED-sense
|
||||
// fields directly — toggleDefault stays `true` because retail's own
|
||||
|
|
@ -456,7 +573,10 @@ public static class ConfigOptionsPageController
|
|||
OptionPage page,
|
||||
Func<uint, uint, string?> resolveString,
|
||||
Bindings bindings,
|
||||
ref DisplaySettings display)
|
||||
ref DisplaySettings display,
|
||||
Func<uint, (uint tex, int w, int h)>? resolveSprite,
|
||||
UiDatFont? datFont,
|
||||
BitmapFont? debugFont)
|
||||
{
|
||||
BuildHeaderRow(listBox, "ID_Graphics_GraphicsSection", resolveString);
|
||||
|
||||
|
|
@ -484,7 +604,8 @@ public static class ConfigOptionsPageController
|
|||
read: () => bindings.LoadDisplay().Resolution,
|
||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { Resolution = value }),
|
||||
defaultValue: "800x600",
|
||||
storeOnly: false); // LIVE
|
||||
storeOnly: false, // LIVE
|
||||
resolveSprite, datFont, debugFont);
|
||||
|
||||
BuildToggleRow(
|
||||
listBox, "ID_Rendering_FullScreen", defaultValue: true, page, resolveString,
|
||||
|
|
@ -563,7 +684,10 @@ public static class ConfigOptionsPageController
|
|||
OptionPage page,
|
||||
Func<uint, uint, string?> resolveString,
|
||||
Bindings bindings,
|
||||
ref DisplaySettings display)
|
||||
ref DisplaySettings display,
|
||||
Func<uint, (uint tex, int w, int h)>? resolveSprite,
|
||||
UiDatFont? datFont,
|
||||
BitmapFont? debugFont)
|
||||
{
|
||||
BuildHeaderRow(listBox, "ID_Graphics_TextureSection", resolveString);
|
||||
|
||||
|
|
@ -575,21 +699,24 @@ public static class ConfigOptionsPageController
|
|||
read: () => bindings.LoadDisplay().LandscapeTextureDetail,
|
||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { LandscapeTextureDetail = value }),
|
||||
defaultValue: 2,
|
||||
storeOnly: true); // AP-198
|
||||
storeOnly: true, // AP-198
|
||||
resolveSprite, datFont, debugFont);
|
||||
|
||||
BuildMenuRow(
|
||||
listBox, "ID_Graphics_EnvironmentTextureDetail", TextureDetailChoices, page, resolveString,
|
||||
read: () => bindings.LoadDisplay().EnvironmentTextureDetail,
|
||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { EnvironmentTextureDetail = value }),
|
||||
defaultValue: 1,
|
||||
storeOnly: true); // AP-198
|
||||
storeOnly: true, // AP-198
|
||||
resolveSprite, datFont, debugFont);
|
||||
|
||||
BuildMenuRow(
|
||||
listBox, "ID_Graphics_TextureFiltering", TextureFilteringChoices, page, resolveString,
|
||||
read: () => bindings.LoadDisplay().TextureFiltering,
|
||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { TextureFiltering = value }),
|
||||
defaultValue: 1,
|
||||
storeOnly: true); // AP-198
|
||||
storeOnly: true, // AP-198
|
||||
resolveSprite, datFont, debugFont);
|
||||
|
||||
// UNRESOLVED (see class doc / register row): retail's own
|
||||
// SetDefaultValue(8) does not index this 6-entry choice array.
|
||||
|
|
@ -600,7 +727,8 @@ public static class ConfigOptionsPageController
|
|||
read: () => bindings.LoadDisplay().LandscapeDrawDistance,
|
||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { LandscapeDrawDistance = value }),
|
||||
defaultValue: 8,
|
||||
storeOnly: true); // AP-198
|
||||
storeOnly: true, // AP-198
|
||||
resolveSprite, datFont, debugFont);
|
||||
|
||||
BuildToggleRow(
|
||||
listBox, "ID_Graphics_BuildingDetailTextures", defaultValue: true, page, resolveString,
|
||||
|
|
@ -694,7 +822,10 @@ public static class ConfigOptionsPageController
|
|||
OptionPage page,
|
||||
Func<uint, uint, string?> resolveString,
|
||||
Bindings bindings,
|
||||
ref ChatSettings chat)
|
||||
ref ChatSettings chat,
|
||||
Func<uint, (uint tex, int w, int h)>? resolveSprite,
|
||||
UiDatFont? datFont,
|
||||
BitmapFont? debugFont)
|
||||
{
|
||||
BuildHeaderRow(listBox, "ID_UI_UISection", resolveString);
|
||||
|
||||
|
|
@ -707,14 +838,16 @@ public static class ConfigOptionsPageController
|
|||
read: () => bindings.LoadChat().ChatFontFace,
|
||||
apply: value => bindings.SaveChat(bindings.LoadChat() with { ChatFontFace = value }),
|
||||
defaultValue: 2,
|
||||
storeOnly: true); // AP-200
|
||||
storeOnly: true, // AP-200
|
||||
resolveSprite, datFont, debugFont);
|
||||
|
||||
BuildMenuRow(
|
||||
listBox, "ID_UI_ChatFontSize", ChatFontSizeChoices, page, resolveString,
|
||||
read: () => bindings.LoadChat().ChatFontSizeIndex,
|
||||
apply: value => bindings.SaveChat(bindings.LoadChat() with { ChatFontSizeIndex = value }),
|
||||
defaultValue: 1,
|
||||
storeOnly: true); // AP-200
|
||||
storeOnly: true, // AP-200
|
||||
resolveSprite, datFont, debugFont);
|
||||
|
||||
chat = bindings.LoadChat();
|
||||
}
|
||||
|
|
@ -990,7 +1123,10 @@ public static class ConfigOptionsPageController
|
|||
Func<int> read,
|
||||
Action<int> apply,
|
||||
int defaultValue,
|
||||
bool storeOnly)
|
||||
bool storeOnly,
|
||||
Func<uint, (uint tex, int w, int h)>? resolveSprite,
|
||||
UiDatFont? datFont,
|
||||
BitmapFont? debugFont)
|
||||
{
|
||||
UiElement? row = listBox.AddItemFromTemplateList(MenuTemplateIndex);
|
||||
if (row is null)
|
||||
|
|
@ -1012,6 +1148,10 @@ public static class ConfigOptionsPageController
|
|||
return;
|
||||
}
|
||||
|
||||
// #378: wire the dropdown chrome — the button-well face, arrow cap,
|
||||
// and scrollable popup — see MenuChromeSprites' own doc.
|
||||
ApplyMenuChrome(menu, resolveSprite, datFont, debugFont);
|
||||
|
||||
// OP6 rework (review S3): the menu button IS the interactive/
|
||||
// hoverable widget for this row.
|
||||
string? tooltip = ResolveTooltip(labelKey, resolveString);
|
||||
|
|
@ -1073,7 +1213,10 @@ public static class ConfigOptionsPageController
|
|||
Func<string> read,
|
||||
Action<string> apply,
|
||||
string defaultValue,
|
||||
bool storeOnly)
|
||||
bool storeOnly,
|
||||
Func<uint, (uint tex, int w, int h)>? resolveSprite,
|
||||
UiDatFont? datFont,
|
||||
BitmapFont? debugFont)
|
||||
{
|
||||
UiElement? row = listBox.AddItemFromTemplateList(MenuTemplateIndex);
|
||||
if (row is null)
|
||||
|
|
@ -1095,6 +1238,10 @@ public static class ConfigOptionsPageController
|
|||
return;
|
||||
}
|
||||
|
||||
// #378: wire the dropdown chrome — the button-well face, arrow cap,
|
||||
// and scrollable popup — see MenuChromeSprites' own doc.
|
||||
ApplyMenuChrome(menu, resolveSprite, datFont, debugFont);
|
||||
|
||||
// OP6 rework (review S3): the menu button IS the interactive/
|
||||
// hoverable widget for this row.
|
||||
string? tooltip = ResolveTooltip(labelKey, resolveString);
|
||||
|
|
|
|||
|
|
@ -2202,7 +2202,15 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
// silently clobber CH6's filter/opacity edits with a
|
||||
// stale snapshot the next time either surface saves.
|
||||
LoadChat: () => _bindings.Chat.Store?.LoadChat() ?? ChatSettings.Default,
|
||||
SaveChat: chat => _bindings.Chat.Store?.SaveChat(chat)));
|
||||
SaveChat: chat => _bindings.Chat.Store?.SaveChat(chat)),
|
||||
// #378: the eight Config-tab dropdown menus need the SAME
|
||||
// sprite/font resolvers every other retail-menu consumer
|
||||
// (ChatWindowController's channel menu, VendorUiController's
|
||||
// category dropdown) already gets — see
|
||||
// ConfigOptionsPageController.MenuChromeSprites' own doc.
|
||||
resolveSprite: _bindings.Assets.ResolveSprite,
|
||||
datFont: _bindings.Assets.DefaultFont,
|
||||
debugFont: _bindings.Assets.DebugFont);
|
||||
if (!configBound)
|
||||
Console.WriteLine("[UI] options panel: Config tab rows did not bind.");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue