fix(vtank): slice 7 round E item D-3 — Advanced Options popup actually resizable

mosstank.xml's own top-of-file comment already claimed the Advanced
Options popup "gets its own resizable=\"true\" + anchored lists" — the
popup file itself still carried the dead resize="none" attribute
(MarkupDocument.cs: "resize" only ever means anything once
resizable="true" already arms the master switch), so the claim was
false and the popup was permanently fixed-size.

resizable="true" now floors minw/minh at the pre-fix authored size
(392x450 — never shrink below the tested layout, matching the main
panel's own rule). lOptionList grows WIDTH ONLY: the value-edit field,
description, and notice all sit directly below it with zero vertical
slack and none of them anchor to shift down in lockstep, so any height
growth here would walk the list straight into the field below — the
same "grow only in the direction with nothing in the way" principle the
main panel already applies to Items/Consumables/Buffs/Route's lists.
lFilterList (the category checklist beside it) tracks the growing right
edge via its own anchor="right" so the widening option list never walks
into it, matching the main panel's own right-pinned-sibling pattern.
AdvancedOptionDescription is declared anchor="left top right" for
markup consistency, but documented as a real no-op: UiLabel.OnDraw
(UiPanel.cs) always overwrites Width/Height to the current measured
text extent on every draw, right after the anchor pass runs in the same
frame, so nothing a label's anchor computes is ever visible — a
genuinely working vertical anchor was deliberately avoided since the
notice label directly below isn't anchored either and a large resize
would walk description into it; fully anchoring the rest of the
vertical stack is a larger redesign left for a future round.

Mutation shown to fail first: reverting the markup (git checkout, patch
saved and reapplied) failed at Assert.True(built.Resizable) — the popup
was still fixed-size.

App markup/plugin filter 243 -> 244; MossTank suite holds 721/721
(this item touches App-side markup only).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-07 18:13:06 +02:00
parent 3fe3043599
commit 64b62fea33
2 changed files with 115 additions and 5 deletions

View file

@ -97,10 +97,51 @@
+/- stepper (a real duplicate of Options' own editable
FollowNavMinimumValueText field). Bold text isn't representable in the
plain retail UI font (0x40000000 has no bold face) — the section
header uses the same bright caption color other headers use instead. -->
header uses the same bright caption color other headers use instead.
D-3 (round E architecture re-check): mosstank.xml's own top-of-file
comment already claimed this popup "gets its own resizable=\"true\" +
anchored lists" — this file never actually carried that; "resize" is a
vestigial axis-lock attribute that only means anything once
resizable="true" already arms the master switch (MarkupDocument.cs),
so resize="none" alone left the popup permanently fixed-size. Now
resizable="true" with minw/minh floored at the pre-fix authored size
(392x450, the same "never shrink below the tested layout" rule the
main panel's own minw/minh follow).
lOptionList grows WIDTH ONLY (anchor="left top right"), NOT height:
the value-edit field/description/notice/"MossTank Extras" section all
sit directly below it with no vertical slack (field starts at y=168,
4px under the list's own y=4+h=164) and none of them are anchored to
shift down in lockstep, so any HEIGHT growth here would walk the list
straight into the field below it — the main panel's own
Items/Consumables/Buffs/Route lists avoid exactly this by growing
height-only where nothing sits below, and by growing WIDTH-only here
instead, this popup gets the same "the growth direction with nothing
in its path" safety. lFilterList (the category checklist, sitting
immediately to the option list's right) is repositioned to track the
right edge via its own anchor="right" so the widening option list
never grows into it — the same "right-pinned sibling repositions"
pattern the main panel's Consumables/Buffs lists already use.
AdvancedOptionDescription is declared anchor="left top right" too, for
markup consistency with the option list above it, but this is a
documented NO-OP for a <label>: UiLabel.OnDraw (UiPanel.cs) always
overwrites Width/Height to the CURRENT measured text extent on every
single draw, immediately after the parent's own anchor pass runs (both
happen inside the same DrawSelfAndChildren frame) — so whatever a
label's anchor computes is discarded before it is ever visible. A
genuinely working vertical anchor (e.g. "bottom", tracking the growing
window edge) was deliberately NOT used here either: with the notice
label directly below it also unanchored, a large enough resize would
walk description right into notice — the same class of risk this
round avoided for the option list. Anchoring the rest of the vertical
stack in lockstep is a real, larger redesign left for a future round;
this one stays inside "make it actually resizable without introducing
a new overlap," per the round's own stated scope. -->
<panel x="253" y="405" w="392" h="450" title="MossTank Advanced Options"
visible="{AdvancedOptionsVisible}" resize="none">
<list x="4" y="4" w="256" h="160" rowheight="17"
visible="{AdvancedOptionsVisible}" resizable="true" minw="392" minh="450">
<list x="4" y="4" w="256" h="160" rowheight="17" anchor="left top right"
selected="{SelectedAdvancedOptionIndex}" onchange="{SelectAdvancedOption}"
tooltip="Select one of Virindi Tank's 137 advanced options.">
<column type="text" width="187" items="{AdvancedOptionNames}" onclick="{SelectAdvancedOption}" />
@ -110,7 +151,7 @@
bug here: <list> markup requires a "selected" int binding even for a
pure checklist with no real selection concept — see
SelectedAdvancedOptionCategoryIndex's own doc comment. -->
<list x="268" y="4" w="120" h="180" rowheight="18"
<list x="268" y="4" w="120" h="180" rowheight="18" anchor="right"
selected="{SelectedAdvancedOptionCategoryIndex}"
tooltip="Uncheck a category to hide its settings from the list on the left.">
<column type="check" width="20" values="{AdvancedOptionCategoryEnabled}" onchange="{ToggleAdvancedOptionCategoryAt}" />
@ -120,7 +161,8 @@
onchange="{SetAdvancedOptionValueDraft}" onsubmit="{SubmitAdvancedOption}"
maxlength="160" clearonsubmit="false" background="#E6000000"
tooltip="Edit the selected advanced-option value and press Enter to apply." />
<label x="4" y="188" w="384" h="80" text="{AdvancedOptionDescription}" color="#FFE8DEC3" />
<label x="4" y="188" w="384" h="80" text="{AdvancedOptionDescription}" color="#FFE8DEC3"
anchor="left top right" />
<label x="4" y="272" w="384" h="16" text="{AdvancedOptionNotice}" color="#FFC7B98F" />
<label x="4" y="294" w="200" h="16" text="MossTank Extras" color="#FFE8DEC3" />

View file

@ -113,6 +113,74 @@ public sealed class MossTankMarkupBuildOverRealFilesTests
$"Monsters list width did not grow: {widthAtAuthoredDefault} -> {monstersList.Width}");
}
/// <summary>
/// D-3 (round E architecture re-check): mosstank.xml's own top-of-file
/// comment already claimed the Advanced Options popup "gets its own
/// resizable=\"true\" + anchored lists" — the popup file itself still
/// carried the dead <c>resize="none"</c> attribute (only meaningful
/// once <c>resizable="true"</c> already arms the master switch,
/// MarkupDocument.cs), so the popup was permanently fixed-size. This
/// proves the fix through the real anchor machinery (the same
/// mechanism <see cref="WideningTheRealMainPanelWidensTheRealMonstersList"/>
/// proves for the main panel): lOptionList grows in WIDTH ONLY
/// (mosstank-advanced.xml's own comment explains why height growth is
/// unsafe here — the value field sits directly below with no slack),
/// and lFilterList tracks the growing right edge in lockstep so the
/// widening option list never walks into it (the same "right-pinned
/// sibling repositions" pattern the main panel already uses).
/// </summary>
[Fact]
public void WideningTheRealAdvancedOptionsPopupGrowsTheOptionListWithoutOverlappingItsSibling()
{
string xml = File.ReadAllText(
Path.Combine(MossTankMarkupDirectory, "mosstank-advanced.xml"));
var panel = new MossTankPanel(new StubHost());
UiNineSlicePanel built = MarkupDocument.Build(xml, panel, static id => (id, 32, 32));
Assert.True(built.Resizable);
Assert.Equal(392f, built.MinWidth);
Assert.Equal(450f, built.MinHeight);
// Bypass the VisibleSource binding (bound to AdvancedOptionsVisible,
// false on the stub automation) the same way the Monsters test
// bypasses tab visibility — DrawSelfAndChildren's own anchor pass
// never runs for an invisible element.
built.Visible = true;
UiMarkupList optionList = Assert.Single(
built.Children.OfType<UiMarkupList>(), static list => list.Width == 256f);
UiMarkupList categoryList = Assert.Single(
built.Children.OfType<UiMarkupList>(), static list => list.Width == 120f);
var device = new RecordingGpuDevice();
var renderer = new TextRenderer(device, new NullGpuFrameSource(), "unused");
renderer.Begin(new Vector2(1400f, 900f));
var ctx = new UiRenderContext(renderer, new Vector2(1400f, 900f));
built.DrawSelfAndChildren(ctx);
float optionListWidthBefore = optionList.Width;
float optionListHeightBefore = optionList.Height;
float categoryListLeftBefore = categoryList.Left;
float categoryListWidthBefore = categoryList.Width;
built.Width += 100f;
built.DrawSelfAndChildren(ctx);
Assert.True(
optionList.Width > optionListWidthBefore,
$"Option list width did not grow: {optionListWidthBefore} -> {optionList.Width}");
Assert.Equal(optionListHeightBefore, optionList.Height); // height fixed
Assert.True(
categoryList.Left > categoryListLeftBefore,
$"Category list did not track the growing right edge: {categoryListLeftBefore} -> {categoryList.Left}");
Assert.Equal(categoryListWidthBefore, categoryList.Width); // width fixed, only repositions
Assert.True(
optionList.Left + optionList.Width <= categoryList.Left,
$"Widened option list (right edge {optionList.Left + optionList.Width}) overlaps "
+ $"the repositioned category list (left edge {categoryList.Left}).");
}
private sealed class NullGpuFrameSource : ICurrentGpuFrameSource
{
public IGpuFrame? CurrentFrame => null;