fix #381: Options-panel footer needs an opaque backing field
Root cause: a live-DAT probe found retail authors NO backing element behind the Character/Chat/Config tabs' Apply/Reset/Defaults footer — each page root has exactly five children (the row ListBox, its scrollbar, and the three buttons) with zero direct-state media on the root itself. Scrolled row content therefore bled through visibly between/behind the three buttons; the bleed-through is a rendering gap in our own composition, not a missing import. Fix: new minimal widget UiSolidSpriteFill tiles RetailChromeSprites.CenterFill (the SAME panel-background sprite the Options window's own chrome already draws behind everything, not an invented color) across the footer strip's rect, derived from the three buttons' own resolved Top/Height and z-ordered strictly behind every other child so it can never intercept input or occlude the buttons. Register row AP-205 records the synthesis. Regressed by OptionsPanelControllerTests. Bind_SynthesizesOneOpaqueFooterBacking_PerPageWithApplyResetDefaults, which pins exactly one backing field per page, sized from the live button rects, z-ordered behind every sibling. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
2a248c0d48
commit
a31fd631ad
7 changed files with 234 additions and 17 deletions
|
|
@ -169,9 +169,18 @@ public sealed class OptionsPanelController : IRetainedPanelController
|
|||
/// import in this codebase (<see cref="CharacterController"/>,
|
||||
/// <see cref="RetailDialogFactory"/>'s dialog catalog).
|
||||
/// </summary>
|
||||
/// <param name="resolveSprite">#381: RenderSurface id → (GL tex handle,
|
||||
/// pixel width, pixel height) — wired onto each page's synthesized
|
||||
/// footer backing field (<see cref="UiSolidSpriteFill"/>). Null leaves
|
||||
/// the field present but drawing nothing (matching every other
|
||||
/// null-safe sprite resolver in this codebase) — the fixture/
|
||||
/// conformance callers that never exercise the visual layer.</param>
|
||||
/// <returns>Null if <paramref name="layout"/>'s root did not build as a
|
||||
/// <see cref="UiTabPanel"/> (a missing/malformed LayoutDesc).</returns>
|
||||
public static OptionsPanelController? Bind(ImportedLayout layout, Callbacks callbacks)
|
||||
public static OptionsPanelController? Bind(
|
||||
ImportedLayout layout,
|
||||
Callbacks callbacks,
|
||||
Func<uint, (uint tex, int w, int h)>? resolveSprite = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(layout);
|
||||
ArgumentNullException.ThrowIfNull(callbacks);
|
||||
|
|
@ -223,7 +232,16 @@ public sealed class OptionsPanelController : IRetainedPanelController
|
|||
|
||||
UiButton? apply = BindPageButton(pageRoot, ApplyButtonId, page.Apply);
|
||||
UiButton? reset = BindPageButton(pageRoot, ResetButtonId, page.Reset);
|
||||
BindPageButton(pageRoot, DefaultsButtonId, page.Defaults);
|
||||
UiButton? defaults = BindPageButton(pageRoot, DefaultsButtonId, page.Defaults);
|
||||
|
||||
// #381 (2026-08-11, gate 4): the opaque backing field behind the
|
||||
// footer strip — see UiSolidSpriteFill's own doc for the
|
||||
// live-DAT probe proving retail authors no such element (the
|
||||
// page root has exactly 5 children, zero direct-state media).
|
||||
// Sized from the THREE BUTTONS' OWN resolved rects (not a
|
||||
// hardcoded literal) so it tracks whatever the authored layout
|
||||
// actually placed them at.
|
||||
AddFooterBacking(pageRoot, apply, reset, defaults, resolveSprite);
|
||||
|
||||
// MUST-FIX 2 (OP4 review-fix round, 2026-08-11): retail
|
||||
// PlayerOptionPage::OnOptionChanged @0x004F27D0 — Apply/Reset
|
||||
|
|
@ -264,6 +282,53 @@ public sealed class OptionsPanelController : IRetainedPanelController
|
|||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Draws behind everything else already added to
|
||||
/// <paramref name="pageRoot"/> — see <see cref="UiSolidSpriteFill"/>'s
|
||||
/// own doc for why this element exists at all (AP-205, synthesized,
|
||||
/// no authored counterpart).</summary>
|
||||
private const int FooterBackingZOrder = int.MinValue / 2;
|
||||
|
||||
/// <summary>
|
||||
/// #381: inserts the opaque, borderless backing field behind
|
||||
/// Apply/Reset/Defaults, deriving its Y/height from whichever of the
|
||||
/// three physical buttons resolved (all three share the SAME authored
|
||||
/// Y=564/Height=32, research doc §3.1 — any one is sufficient). Skipped
|
||||
/// (not an error — logged) only when NONE resolved; a page missing all
|
||||
/// three already logged its own per-button warnings above.
|
||||
/// </summary>
|
||||
private static void AddFooterBacking(
|
||||
UiElement pageRoot,
|
||||
UiButton? apply,
|
||||
UiButton? reset,
|
||||
UiButton? defaults,
|
||||
Func<uint, (uint tex, int w, int h)>? resolveSprite)
|
||||
{
|
||||
UiButton? first = apply ?? reset ?? defaults;
|
||||
if (first is null)
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"[D.2b] OptionsPanelController: page 0x{pageRoot.DatElementId:X8} has no "
|
||||
+ "resolved Apply/Reset/Defaults buttons — footer backing field skipped.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Span the FULL authored page width (not just the button union) —
|
||||
// retail's own three buttons don't reach the page's right edge, but
|
||||
// the reported bleed-through is content scrolling behind the WHOLE
|
||||
// footer strip, not just the button rects themselves.
|
||||
var backing = new UiSolidSpriteFill
|
||||
{
|
||||
SpriteId = RetailChromeSprites.CenterFill,
|
||||
SpriteResolve = resolveSprite,
|
||||
Left = 0f,
|
||||
Top = first.Top,
|
||||
Width = pageRoot.Width,
|
||||
Height = first.Height,
|
||||
ZOrder = FooterBackingZOrder,
|
||||
};
|
||||
pageRoot.AddChild(backing);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Activates the tab-switching behavior (idempotent — safe even if
|
||||
/// already active). Must run AFTER <see cref="Bind"/> so this
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue