fix(vtank): slice 7 round D item 4 — resizable panel, larger default

Owner live report 2026-09-07: "The size of the entire window needs to
be enlarged for default and should also be resizeable." mosstank.xml's
main panel is now resizable="true" with minw/minh floored at the
PRE-round-D authored size (856x236) and a new default authored size
~15% larger in both directions (984x271) — every one of the nine tab
groups grows by the identical 128px-wide/35px-tall delta (848x194 ->
976x229), preserving the original 0px group-to-panel margin design
exactly.

Every tab <group> gets anchor="left top right bottom" so a future
manual drag-resize keeps growing it. The two single-list tabs with
nothing beside their list (Monsters, Meta) give their list the SAME
full stretch, widened AND heightened by the same 128x35 delta at
author time — anchors only react to CHANGE from their first-draw
baseline, so bumping only the outer group while leaving the list's own
geometry untouched would leave it visually unchanged at the new
default (the growth invisibly reserved as margin until a further
manual resize). Every other tab's list(s) grow in HEIGHT only (+35),
pinned to whichever horizontal edge doesn't have a sibling list/button
column in the way (Items/Consumables/Buffs/Route all have one) — a
right-pinned sibling (Consumables' Excluded Scarab list, Buffs'
Blacklisted Buff Families list) is repositioned +128 at author time so
it already sits flush against the enlarged default's right edge.
Trailing "Add" rows/status labels below a taller list shift down by
the same +35 and get anchor="bottom" to keep tracking. Options/
Profiles/Vitals need no internal changes — only their own group grows.

Deviations, both documented in mosstank.xml's own Round D item 4
comment: Monsters' extra 128px of list width lands in its LAST column
(the MoveDown icon, which always absorbs the remainder per the markup
grammar) — a wider icon cell, not a redesigned grid; seven of nine
tabs' groups grow wider than their content uses, leaving harmless
empty space on the right at the enlarged default rather than
redesigning nine tabs' pixel geometry in one pass (a deliberate,
minimal-risk choice — nothing moved INTO another control's space, only
into previously-empty margin, so every existing AssertNoSiblingOverlap/
AssertWithinParent guarantee still holds).

Tests added:
- MossTankMarkupContractTests.AuthoredShellFitsTheMinimumCanvasAnd...
  updated to the new 984x271 default.
- PanelIsResizableFlooredAtThePreRoundDAuthoredSize (resizable="true",
  minw/minh = 856/236).
- EveryStretchingListDeclaresARealAnchor (contract pin: every list in
  Monsters/Items/Consumables/Buffs/Route/Meta declares a real anchor
  attribute, not the silent left-top default).
- MossTankMarkupBuildOverRealFilesTests.
  WideningTheRealMainPanelWidensTheRealMonstersList (App.Tests): builds
  the REAL mosstank.xml against a real MossTankPanel, widens the built
  root by 100px, and confirms the Monsters list's own Width grows in
  turn — the same mechanism MarkupResizableAnchorTests already proves
  against synthetic markup, now proven against the shipped file.

Mutation shown to fail: temporarily removing the Monsters list's own
anchor attribute failed the new re-layout test (976 -> 976, no growth);
restored and confirmed green. A build-copy gotcha surfaced while
writing these tests: dotnet build's default incremental copy did NOT
refresh the test projects' deployed mosstank.xml after editing the
source file (PreserveNewest apparently didn't see it as newer under
normal incremental evaluation) — dotnet build --no-incremental was
needed to get a fresh copy into bin/ before the size/anchor tests
would actually exercise the new markup instead of a stale cached copy.

Verified: dotnet build AcDream.slnx -c Release (clean, --no-incremental)
green; MossTank suite 715/715 (713 -> 715, two new markup contract
pins); App markup/plugin filter 243/243 (242 -> 243, one new re-layout
test).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-07 17:24:31 +02:00
parent 5318adbb30
commit 2d626971d9
3 changed files with 286 additions and 65 deletions

View file

@ -50,8 +50,60 @@
248->252) as a small safety margin since it was the tightest already-OK
case measured.
-->
<panel x="28" y="42" w="856" h="236" title="MossTank v0.1.0"
visible="{WindowAvailable}" resize="none">
<!--
Round D item 4 (owner: "The size of the entire window needs to be
enlarged for default and should also be resizeable"): the panel is now
resizable="true" with minw/minh pinned to the PRE-round-D authored size
(856x236, the floor a drag-resize or a persisted-layout restore will
never shrink below) and a new default authored size ~15% larger in both
directions (984x271) — the SAME uniform 128px-wide/35px-tall delta every
one of the nine tab groups below also grows by (848x194 -> 976x229),
preserving the original 0px group-to-panel margin design exactly
(976 = 984-8, 229 = 271-42, matching 848 = 856-8, 194 = 236-42).
Anchors follow the doc's own rule ("give the group anchor=... so it
stretches with the panel, and give a <list> inside it anchor=... so the
list stretches with the GROUP's width in turn"): every tab <group> gets
anchor="left top right bottom" so a future manual drag-resize keeps
growing it. The two single-list, nothing-to-its-side tabs (Monsters,
Meta) give their list the SAME full stretch, widened AND heightened by
the identical 128x35 delta at author time so the enlarged DEFAULT
already shows a filled list, not a list floating in a corner with dead
space baked in as a captured margin (anchors only react to CHANGE from
their first-draw baseline — bumping only the outer group's size while
leaving a list's authored geometry untouched would leave it visually
unchanged at the new default, all the growth invisibly reserved as
"margin" until a further manual resize). Every OTHER tab's list(s) grow
in HEIGHT only (+35, matching the group), pinned to their original
horizontal edge (anchor="left ... bottom" for a left list, "right ...
bottom" for a right one) — height-only growth is safe everywhere
(nothing sits below a list that a taller list would collide with, once
the trailing "Add" row/status labels below it are shifted down by the
same +35 and anchored "bottom" to keep tracking) while WIDTH growth
risks colliding with a sibling list or button column that isn't also
being resized (Items/Consumables/Buffs/Route all have one), so this
round leaves those horizontally fixed rather than guessing a redesign
VTank's own KB tables don't describe. A right-pinned sibling (the
second list in Consumables/Buffs) is REPOSITIONED +128 (the same delta)
at author time so it already sits flush against the new default's right
edge instead of floating mid-window until a manual resize catches up,
matching the same "capture the FINAL position, not the pre-growth one"
principle. Options/Profiles/Vitals need no internal changes (no <list>,
every control already fits comfortably) — only their own group grows,
leaving harmless empty space below/right at the bigger default; this is
a deliberate, minimal-risk choice over redesigning nine tabs' pixel
geometry in one pass; every AssertNoSiblingOverlap/AssertWithinParent
guarantee this round doesn't touch continues to hold since nothing
moved INTO another control's space, only into previously-empty margin.
The two secondary popups (mosstank-loot-editor.xml, mosstank-
buffpicker.xml, mosstank-metaeditor.xml) keep their fixed VTank-matched
sizes per the plan's own scope; the Advanced Options popup
(mosstank-advanced.xml) is the one exception and gets its own
resizable="true" + anchored lists, per its own file's comment.
-->
<panel x="28" y="42" w="984" h="271" title="MossTank v0.1.0"
visible="{WindowAvailable}" resizable="true" minw="856" minh="236">
<!-- Virindi Tank tab order and compact text-strip presentation. -->
<tab x="8" y="22" w="58" h="18" text="Options"
selected="{OptionsSelected}" enabled="{OptionsTabEnabled}" onclick="{ShowOptions}" />
@ -80,7 +132,7 @@
settings that already existed (only reachable through the generic
Advanced Options key-value editor) and now also get VTank's own
direct checkbox. -->
<group x="8" y="42" w="848" h="194" visible="{OptionsVisible}">
<group x="8" y="42" w="976" h="229" visible="{OptionsVisible}" anchor="left top right bottom">
<label x="8" y="8" w="80" h="16" text="Monster Range:" color="#FFE8DEC3" />
<field x="8" y="24" w="40" h="16" text="{MonsterRangeValueText}"
onchange="{SetMonsterRangeText}" maxlength="6" background="#E6000000"
@ -212,7 +264,7 @@
Editor, Meta has only CopyTo) — they now share one column (x=566),
leaving a wider gap on the shorter rows rather than four columns
that don't line up. -->
<group x="8" y="42" w="848" h="194" visible="{ProfilesVisible}">
<group x="8" y="42" w="976" h="229" visible="{ProfilesVisible}" anchor="left top right bottom">
<label x="8" y="8" w="142" h="16" text="Macro Settings" color="#FFE8DEC3" />
<menu x="156" y="8" w="140" h="20" items="{MacroProfileNames}"
selected="{SelectedMacroProfile}" onchange="{SelectMacroProfile}"
@ -271,7 +323,7 @@
only span x=8-328; "Enable vital recharge"/"Help fellows" (no VTank
equivalent — MossTank-only features) and the status labels sit to
the right of that, in space VTank's own layout never uses. -->
<group x="8" y="42" w="848" h="194" visible="{VitalsVisible}">
<group x="8" y="42" w="976" h="229" visible="{VitalsVisible}" anchor="left top right bottom">
<label x="8" y="0" w="80" h="16" text="Heal at:" color="#FFE8DEC3" />
<slider x="100" y="0" w="144" h="16" min="0" max="100"
value="{NormalHealthPercent}" onchange="{SetNormalHealthPercent}"
@ -338,7 +390,7 @@
cell is its own click target, exactly like VTank's own row (VVS's
HudList has no header row either — the 15 <label>s below are
ordinary StaticText placed above the list, same as mainView.xml). -->
<group x="8" y="42" w="848" h="194" visible="{MonstersVisible}">
<group x="8" y="42" w="976" h="229" visible="{MonstersVisible}" anchor="left top right bottom">
<label x="0" y="0" w="20" text="F" color="#FFE8DEC3" tooltip="Fester" />
<label x="20" y="0" w="20" text="B" color="#FFE8DEC3" tooltip="Broadside of a Barn" />
<label x="40" y="0" w="20" text="G" color="#FFE8DEC3" tooltip="Gravity Well" />
@ -374,7 +426,14 @@
<!-- clMoveUp/clMoveDown (#22/#23) carry no header label in VTank's own
mainView.xml either (KB table: "-" for both) — skipped to match. -->
<list x="0" y="16" w="848" h="116"
<!-- Round D item 4: full stretch (Monsters has nothing to either side
of its one full-width list) — widened/heightened by the same
128x35 delta the group grows by so the enlarged default already
shows a filled list; the extra 128px lands in the LAST column
(MoveDown icon), which always absorbs the remainder regardless of
its own declared width (docs/plugin-ui-markup.md's Columns
section) — a wider icon cell, not a broken one. -->
<list x="0" y="16" w="976" h="151" anchor="left right top bottom"
selected="{SelectedMonsterListRow}" onchange="{SelectMonsterListRow}"
tooltip="Click a check to toggle it, a name to delete the rule, or another cell to cycle its value.">
<column type="check" width="20" values="{MonsterFesterColumn}" onchange="{ToggleMonsterFesterAt}" />
@ -402,13 +461,15 @@
<column type="icon" width="23" iconkind="did" values="{MonsterMoveDownIcons}" onclick="{MoveMonsterRuleDownAt}" />
</list>
<field x="8" y="136" w="388" h="16" text="{MonsterExpressionDraft}"
<field x="8" y="171" w="388" h="16" text="{MonsterExpressionDraft}"
onchange="{SetMonsterExpressionDraft}" maxlength="256"
background="#E6000000" color="#FFE8DEC3"
background="#E6000000" color="#FFE8DEC3" anchor="bottom"
tooltip="Monster name or expression to match." />
<button x="404" y="136" w="50" h="16" text="Add" onclick="{AddMonsterRule}"
<button x="404" y="171" w="50" h="16" text="Add" onclick="{AddMonsterRule}"
anchor="bottom"
tooltip="Add a new monster rule using the name/expression above." />
<button x="462" y="136" w="50" h="16" text="Add Sel" onclick="{AddSelectedMonster}"
<button x="462" y="171" w="50" h="16" text="Add Sel" onclick="{AddSelectedMonster}"
anchor="bottom"
tooltip="Add the monster currently selected in the world." />
</group>
@ -421,20 +482,24 @@
click deletes the row (case 0); hands-cell click cycles handedness
(case 1) — session-local only, see DeleteItemRowAt/CycleItemHandsAt's
own doc comments for why handedness isn't persisted. -->
<group x="8" y="42" w="848" h="194" visible="{ItemsVisible}">
<group x="8" y="42" w="976" h="229" visible="{ItemsVisible}" anchor="left top right bottom">
<label x="4" y="8" w="327" text="Weapons / Wands / Shields / Pets" color="#FFE8DEC3" />
<label x="331" y="8" w="103" text="Hands" color="#FFE8DEC3" />
<list x="4" y="28" w="430" h="112" rowheight="18"
<!-- Round D item 4: height-only growth (+35, matching the group) —
the right-side info block starts at x=438, only 4px past this
list's right edge, so widening it would collide; pinned left,
not stretching horizontally. -->
<list x="4" y="28" w="430" h="147" rowheight="18" anchor="left top bottom"
selected="{SelectedItemRowIndex}" onchange="{SelectItemRow}"
tooltip="Click a name to remove it, or Hands to cycle 1-Handed/2-Handed.">
<column type="text" width="327" items="{ItemNameColumn}" onclick="{DeleteItemRowAt}" />
<column type="text" width="103" items="{ItemHandsColumn}" onclick="{CycleItemHandsAt}" />
</list>
<button x="4" y="154" w="126" h="25" text="Add" onclick="{AddSelectedItem}" />
<button x="138" y="154" w="138" h="25" text="Add (no buffs)"
onclick="{AddSelectedItemNoBuffs}" />
<button x="284" y="154" w="126" h="25" text="Remove"
onclick="{RemoveSelectedItem}" />
<button x="4" y="189" w="126" h="25" text="Add" onclick="{AddSelectedItem}" anchor="bottom" />
<button x="138" y="189" w="138" h="25" text="Add (no buffs)"
onclick="{AddSelectedItemNoBuffs}" anchor="bottom" />
<button x="284" y="189" w="126" h="25" text="Remove"
onclick="{RemoveSelectedItem}" anchor="bottom" />
<label x="438" y="36" text="{MonsterEquipmentText}" color="#FFC7B98F" />
<toggle x="438" y="64" w="220" h="20" text="Refill Worn Mana"
checked="{RefillWornManaEnabled}" onclick="{ToggleRefillWornMana}" />
@ -452,18 +517,24 @@
row; "Add Selected" requires a selected spell-component-class
object, see AddSelectedComponent's own doc comment). PITCH: icon
16+7=23, text column auto ("*", last column). -->
<group x="8" y="42" w="848" h="194" visible="{ConsumablesVisible}">
<group x="8" y="42" w="976" h="229" visible="{ConsumablesVisible}" anchor="left top right bottom">
<label x="4" y="8"
text="Gems / Food / Kits / Potions / Charges / Grenades / Lockpicks"
color="#FFE8DEC3" />
<label x="438" y="8" text="Excluded Scarab Types:" color="#FFE8DEC3" />
<!-- Round D item 4: the right-hand block (header/list/button/notice,
all originally at x=438 against the OLD 848-wide group) shifts by
the SAME +128 delta the group grows by, so it already sits flush
against the enlarged default's right edge instead of floating
mid-window until a manual resize catches up; anchor="right"
keeps it tracking any FURTHER resize the same way. -->
<label x="566" y="8" text="Excluded Scarab Types:" color="#FFE8DEC3" anchor="right" />
<!-- Fix round B item 7: a click removes the row directly
(PluginCore.cs:7683-7700), matching the "click a row to remove it"
convention the right-hand list already uses. -->
<list x="4" y="28" w="414" h="112" rowheight="18"
<list x="4" y="28" w="414" h="147" rowheight="18" anchor="left top bottom"
items="{ConsumableRows}" selected="{SelectedConsumableRowIndex}"
onchange="{SelectConsumableRow}" tooltip="Click a row to remove it." />
<list x="438" y="28" w="406" h="112" rowheight="18"
<list x="566" y="28" w="406" h="147" rowheight="18" anchor="right top bottom"
selected="{SelectedExcludedComponentIndex}" onchange="{SelectExcludedComponentRow}"
tooltip="Click a row to remove it from the blacklist.">
<column type="icon" width="23" iconkind="did" values="{ExcludedComponentIcons}"
@ -471,15 +542,15 @@
<column type="text" width="*" items="{ExcludedComponentRows}"
onclick="{DeleteExcludedComponentAt}" />
</list>
<button x="4" y="154" w="176" h="25" text="Add"
onclick="{AddSelectedConsumable}" />
<button x="188" y="154" w="110" h="25" text="Add All Peas"
onclick="{AddAllPeas}" />
<button x="306" y="154" w="104" h="25" text="Remove"
onclick="{RemoveSelectedConsumable}" />
<button x="438" y="154" w="176" h="24" text="Add Selected"
onclick="{AddSelectedComponent}" />
<label x="438" y="178" w="176" h="16" text="{ProfileNotice}" color="#FF9B9072" />
<button x="4" y="189" w="176" h="25" text="Add"
onclick="{AddSelectedConsumable}" anchor="bottom" />
<button x="188" y="189" w="110" h="25" text="Add All Peas"
onclick="{AddAllPeas}" anchor="bottom" />
<button x="306" y="189" w="104" h="25" text="Remove"
onclick="{RemoveSelectedConsumable}" anchor="bottom" />
<button x="566" y="189" w="176" h="24" text="Add Selected"
onclick="{AddSelectedComponent}" anchor="right bottom" />
<label x="566" y="213" w="176" h="16" text="{ProfileNotice}" color="#FF9B9072" anchor="right bottom" />
</group>
<!-- Buffs: VTank's REAL Buffs tab (docs/research/vtank-kb/08-ui-views.md
@ -507,7 +578,7 @@
(194-16, item 3's bottom-fit rule) — a proportion-preserving
compromise fitting a real 184x194 box, not a pixel-for-pixel VVS
port (owner's bar: "looks basically the same", never VVS pixels). -->
<group x="8" y="42" w="848" h="194" visible="{BuffsVisible}">
<group x="8" y="42" w="976" h="229" visible="{BuffsVisible}" anchor="left top right bottom">
<!-- Fix round B item 14 (owner honesty rule): BuffPlan.Build does not
read this set at all yet — a real, tracked wiring gap (see
BuffSettings.ExtraBuffSpellNames' own doc comment; issue #491),
@ -515,19 +586,24 @@
names already affect what gets cast. -->
<label x="4" y="0" w="320" h="16" text="Extra Buff Spells" color="#FFE8DEC3"
tooltip="Named spell exemplars — stored and shown here, not yet used when choosing what to cast (#491)." />
<list x="4" y="18" w="320" h="116" rowheight="17"
<!-- Round D item 4: height-only growth (+35), pinned left. -->
<list x="4" y="18" w="320" h="151" rowheight="17" anchor="left top bottom"
items="{ExtraBuffRows}" selected="{SelectedExtraBuffIndex}"
onchange="{DeleteExtraBuffAt}" tooltip="Click a row to remove it." />
<button x="4" y="138" w="120" h="18" text="Add..." onclick="{ShowExtraBuffPicker}" />
<button x="4" y="173" w="120" h="18" text="Add..." onclick="{ShowExtraBuffPicker}" anchor="bottom" />
<!-- Fix round B item 14: same honesty note as Extra Buff Spells — not
yet read by BuffPlan.Build (issue #491). -->
<label x="524" y="0" w="320" h="16" text="Blacklisted Buff Families" color="#FFE8DEC3"
yet read by BuffPlan.Build (issue #491). Round D item 4: this
whole right-hand block shifts +128 (the group's own growth
delta) so it sits flush against the enlarged default's right
edge, same as Consumables' right list. -->
<label x="652" y="0" w="320" h="16" text="Blacklisted Buff Families" color="#FFE8DEC3"
anchor="right"
tooltip="Named buff families — stored and shown here, not yet used when choosing what to cast (#491)." />
<list x="524" y="18" w="320" h="116" rowheight="17"
<list x="652" y="18" w="320" h="151" rowheight="17" anchor="right top bottom"
items="{BlacklistedBuffFamilyRows}" selected="{SelectedBlacklistedBuffIndex}"
onchange="{DeleteBlacklistedBuffFamilyAt}" tooltip="Click a row to remove it." />
<button x="524" y="138" w="120" h="18" text="Add..." onclick="{ShowBlacklistedBuffPicker}" />
<button x="652" y="173" w="120" h="18" text="Add..." onclick="{ShowBlacklistedBuffPicker}" anchor="right bottom" />
<toggle x="332" y="0" w="180" h="16" text="Trained skills only"
checked="{TrainedOnlyEnabled}" onclick="{ToggleTrainedOnly}" />
@ -557,7 +633,11 @@
<button x="332" y="154" w="110" h="18" text="{BuffButtonText}" onclick="{Buff}" />
<label x="446" y="158" w="70" text="{BuffStatus}" color="#FFC7B98F" />
<label x="332" y="178" w="184" h="16" text="{Coverage}" color="#FF9B9072" />
<!-- Round D item 4: the "bottom-fit rule" the original comment names
(194-16) now targets the enlarged group's own bottom edge
(229-16), anchor="bottom" so a further manual resize keeps it
pinned there. -->
<label x="332" y="213" w="184" h="16" text="{Coverage}" color="#FF9B9072" anchor="bottom" />
</group>
<!-- Route: VTank's own 2-across button grid, row order, and bottom
@ -587,31 +667,34 @@
real editable field (RoutePauseSecondsFieldText, default "5") plus
a "seconds" label, matching VTank's txtPauseWaypointTime/Label52
instead of the old "-"/"+" stepper. -->
<group x="8" y="42" w="848" h="194" visible="{RouteVisible}">
<group x="8" y="42" w="976" h="229" visible="{RouteVisible}" anchor="left top right bottom">
<label x="4" y="0" w="370" h="16" text="Navigation Waypoints" color="#FFE8DEC3" />
<list x="4" y="16" w="370" h="116" rowheight="17"
<!-- Round D item 4: height-only growth (+35) — the Add-button block
starts at x=380, only 6px past this list's right edge, so it
stays pinned left rather than stretching into it. -->
<list x="4" y="16" w="370" h="151" rowheight="17" anchor="left top bottom"
selected="{SelectedRouteWaypointIndex}" onchange="{SelectRouteWaypoint}"
tooltip="Click a waypoint to remove it.">
<column type="text" width="337" items="{RouteWaypointTextColumn}" onclick="{DeleteRouteWaypointAt}" />
<column type="text" width="30" items="{RouteWaypointCountColumn}" onclick="{DeleteRouteWaypointAt}" />
</list>
<menu x="4" y="136" w="80" h="16" items="{RouteModeNames}"
<menu x="4" y="171" w="80" h="16" items="{RouteModeNames}" anchor="bottom"
selected="{SelectedRouteMode}" onchange="{SelectRouteMode}"
rows="4" openupward="true" tooltip="Choose Circular, Linear, Follow, or Once route behavior." />
<menu x="90" y="136" w="120" h="16" items="{RouteInsertModeNames}"
<menu x="90" y="171" w="120" h="16" items="{RouteInsertModeNames}" anchor="bottom"
selected="{SelectedRouteInsertMode}" onchange="{SelectRouteInsertMode}"
rows="3" openupward="true" tooltip="Where Add buttons insert the new waypoint." />
<button x="214" y="136" w="16" h="16" icon="0x060028FD" iconkind="did"
<button x="214" y="171" w="16" h="16" icon="0x060028FD" iconkind="did" anchor="bottom"
onclick="{MoveRouteWaypointDown}" tooltip="Advance the current point." />
<button x="230" y="136" w="16" h="16" icon="0x060028FC" iconkind="did"
<button x="230" y="171" w="16" h="16" icon="0x060028FC" iconkind="did" anchor="bottom"
onclick="{MoveRouteWaypointUp}" tooltip="Regress the current point." />
<!-- VTank's own btnNavResetPoint (icon 0x060011F7, "Select Nearest
Point") reassigns the LIVE navigation cursor; MossTank has no
mutable cursor to expose to a plugin, so this moves the tab's
own edit selection to the closest waypoint instead — see
SelectNearestRouteWaypoint's own doc comment. -->
<button x="246" y="136" w="16" h="16" icon="0x060011F7" iconkind="did"
<button x="246" y="171" w="16" h="16" icon="0x060011F7" iconkind="did" anchor="bottom"
onclick="{SelectNearestRouteWaypoint}" tooltip="Select the nearest waypoint." />
<button x="380" y="16" w="88" h="16" text="Add" onclick="{AddRoutePoint}" />
@ -642,8 +725,8 @@
maxlength="128" clearonsubmit="false" background="#E6000000"
color="#FFE8DEC3" tooltip="Chat command added by Add Chat." />
<label x="4" y="160" w="300" h="16" text="{NavigationStatus}" color="#FF9B9072" />
<label x="310" y="160" w="300" h="16" text="{RouteNotice}" color="#FF9B9072" />
<label x="4" y="195" w="300" h="16" text="{NavigationStatus}" color="#FF9B9072" anchor="bottom" />
<label x="310" y="195" w="300" h="16" text="{RouteNotice}" color="#FF9B9072" anchor="bottom" />
</group>
<!-- Meta: ordered VTank state-machine rules. Rules fire once per state
@ -670,8 +753,14 @@
isn't representable in the plain retail UI font (0x40000000 has no
bold face) — "Current State:" uses the same bright caption color
other tab headers do instead. -->
<group x="8" y="42" w="848" h="194" visible="{MetaVisible}">
<list x="4" y="16" w="840" h="116" rowheight="17"
<group x="8" y="42" w="976" h="229" visible="{MetaVisible}" anchor="left top right bottom">
<!-- Round D item 4: full stretch (Meta has nothing beside its one
full-width list, like Monsters) — widened/heightened by the same
128x35 delta the group grows by. The extra width lands in the
Condition/Action columns' own "*" auto-share (docs/plugin-ui-
markup.md's Columns section), which is the intended fill target,
unlike Monsters' icon-column remainder. -->
<list x="4" y="16" w="968" h="151" rowheight="17" anchor="left right top bottom"
selected="{SelectedMetaRuleIndex}" onchange="{SelectMetaRule}"
tooltip="Click State/Condition/Action to edit; delete/move-up/move-down cells act immediately.">
<column type="text" width="23" items="{MetaDeleteColumn}" onclick="{DeleteMetaRuleAt}" />
@ -682,14 +771,14 @@
<column type="text" width="*" items="{MetaActionColumn}" onclick="{SelectMetaRule}" />
</list>
<label x="4" y="136" w="136" h="16" text="Current State:" color="#FFE8DEC3" />
<menu x="140" y="136" w="120" h="16" items="{MetaCurrentStateNames}"
<label x="4" y="171" w="136" h="16" text="Current State:" color="#FFE8DEC3" anchor="bottom" />
<menu x="140" y="171" w="120" h="16" items="{MetaCurrentStateNames}" anchor="bottom"
selected="{SelectedMetaCurrentState}" onchange="{SetMetaCurrentState}"
rows="8" openupward="true"
tooltip="Force the live Meta engine into a state directly." />
<button x="462" y="136" w="50" h="16" text="Create" onclick="{CreateMetaRule}" />
<button x="462" y="171" w="50" h="16" text="Create" onclick="{CreateMetaRule}" anchor="bottom" />
<label x="4" y="178" w="500" h="16" text="{MetaStatus}" color="#FFC7B98F" />
<label x="4" y="213" w="500" h="16" text="{MetaStatus}" color="#FFC7B98F" anchor="bottom" />
</group>
</panel>

View file

@ -1,3 +1,7 @@
using System.Numerics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Tests.Rendering.Gpu;
using AcDream.App.UI;
using AcDream.Plugin.Abstractions;
using AcDream.Plugins.MossTank;
@ -47,6 +51,73 @@ public sealed class MossTankMarkupBuildOverRealFilesTests
Assert.NotEmpty(built.Children);
}
/// <summary>
/// Round D item 4's own re-layout proof: the Monsters tab's real
/// mosstank.xml list carries <c>anchor="left right top bottom"</c>
/// (EveryStretchingListDeclaresARealAnchor, MossTankMarkupContractTests,
/// pins the attribute is present; this proves the attribute actually
/// DOES something through the real anchor machinery) — widening the
/// built root panel widens the Monsters list in turn, the same
/// mechanism MarkupResizableAnchorTests.ResizingPanel_LeftRightList_
/// WidensWithThePanel proves against synthetic markup, now proven
/// against the real shipped file.
/// </summary>
[Fact]
public void WideningTheRealMainPanelWidensTheRealMonstersList()
{
string xml = File.ReadAllText(
Path.Combine(MossTankMarkupDirectory, "mosstank.xml"));
var panel = new MossTankPanel(new StubHost());
UiNineSlicePanel built = MarkupDocument.Build(xml, panel, static id => (id, 32, 32));
// Every tab's group is visibility-bound ("visible={XVisible}") to
// StubHost's own IsAvailable=false automation, so relying on the
// normal VisibleSource/TickSelfAndChildren reconciliation would
// hide every tab (including the root). This test cares about the
// anchor mechanism, not the tab-switching one — it makes the
// Monsters group (the 4th of the nine tab groups in file order:
// Options/Profiles/Vitals/Monsters/...) visible directly. Exact
// type match, not OfType<UiPanel>() — UiSimpleButton/
// UiMarkupTabButton (the tab strip) are ALSO UiPanel subtypes;
// only a bare <group> compiles to the base UiPanel type itself.
UiPanel[] tabGroups = built.Children
.Where(static child => child.GetType() == typeof(UiPanel))
.Cast<UiPanel>()
.ToArray();
Assert.Equal(9, tabGroups.Length);
foreach (UiPanel group in tabGroups)
group.Visible = false;
UiPanel monstersGroup = tabGroups[3];
monstersGroup.Visible = true;
UiMarkupList monstersList = Assert.Single(monstersGroup.Children.OfType<UiMarkupList>());
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));
// First draw at the authored (already-enlarged, 984 wide) default
// captures the list's anchor baseline.
built.DrawSelfAndChildren(ctx);
float widthAtAuthoredDefault = monstersList.Width;
// A live drag-resize (RetailWindowManager.ResizeTo) mutates Width
// directly; the next draw re-applies the captured margins against
// the NEW panel width.
built.Width += 100f;
built.DrawSelfAndChildren(ctx);
Assert.True(
monstersList.Width > widthAtAuthoredDefault,
$"Monsters list width did not grow: {widthAtAuthoredDefault} -> {monstersList.Width}");
}
private sealed class NullGpuFrameSource : ICurrentGpuFrameSource
{
public IGpuFrame? CurrentFrame => null;
}
private sealed class StubHost : IPluginHost
{
public bool HasUi => false;

View file

@ -259,20 +259,81 @@ public sealed class MossTankMarkupContractTests
Path.Combine(AppContext.BaseDirectory, "mosstank.xml"));
XElement root = Assert.IsType<XElement>(document.Root);
// Campaign VT slice 7 S7.1: 856 wide, matching VTank's own
// mainView.xml content (856x210, docs/research/vtank-kb/
// 08-ui-views.md §0). Fix round A (2026-09-07) moved the Advanced
// Options / Loot Editor popups OUT into their own plugin panels
// (mosstank-advanced.xml / mosstank-loot-editor.xml), so the main
// window is back to VTank's own proportion: 236 tall (42px header +
// tab strip, unchanged since S7.1, + 194px per-tab content,
// unchanged) with no dead space below the tabs — see mosstank.xml's
// own panel-level comment.
Assert.Equal(856f, Number(root, "w"));
Assert.Equal(236f, Number(root, "h"));
// Round D item 4 (owner: "The size of the entire window needs to
// be enlarged for default and should also be resizeable"): the
// authored default grew ~15% in both directions from the S7.1/
// fix-round-A proportion (856x236, still the panel's own
// minw/minh floor — see PanelIsResizableFlooredAtThePreRoundDAuthoredSize
// below) to 984x271. See mosstank.xml's own Round D item 4
// panel-level comment for the exact delta math (128 wide, 35
// tall) every tab group and stretching list shares.
Assert.Equal(984f, Number(root, "w"));
Assert.Equal(271f, Number(root, "h"));
AssertWithinParent(root);
}
/// <summary>
/// Round D item 4: the panel itself is resizable, floored at the
/// PRE-round-D authored size (856x236) — a drag-resize or a
/// persisted-layout restore must never shrink it past what every tab
/// was actually authored/tested at.
/// </summary>
[Fact]
public void PanelIsResizableFlooredAtThePreRoundDAuthoredSize()
{
XDocument document = XDocument.Load(
Path.Combine(AppContext.BaseDirectory, "mosstank.xml"));
XElement root = Assert.IsType<XElement>(document.Root);
Assert.Equal("true", (string?)root.Attribute("resizable"));
Assert.Equal(856f, Number(root, "minw"));
Assert.Equal(236f, Number(root, "minh"));
}
/// <summary>
/// Round D item 4's own contract pin: every list this round calls out
/// as "should grow" (Monsters, Items, Consumables x2, Buffs x2, Route,
/// Meta) declares a real anchor — not the silent "left top" default,
/// which would leave it a fixed size forever regardless of how far the
/// user drags the window. Doesn't prescribe WHICH edges (Monsters/Meta
/// stretch both axes; the others grow height-only, pinned to whichever
/// horizontal edge a sibling list/button column doesn't block — see
/// mosstank.xml's own Round D item 4 comment for why per list).
/// </summary>
[Fact]
public void EveryStretchingListDeclaresARealAnchor()
{
(string GroupVisible, int ExpectedListCount)[] stretchingListGroups =
[
("MonstersVisible", 1),
("ItemsVisible", 1),
("ConsumablesVisible", 2),
("BuffsVisible", 2),
("RouteVisible", 1),
("MetaVisible", 1),
];
XDocument document = XDocument.Load(
Path.Combine(AppContext.BaseDirectory, "mosstank.xml"));
XElement root = Assert.IsType<XElement>(document.Root);
foreach ((string groupVisible, int expectedListCount) in stretchingListGroups)
{
XElement group = root.Elements("group")
.Single(g => (string?)g.Attribute("visible") == $"{{{groupVisible}}}");
XElement[] lists = group.Elements("list").ToArray();
Assert.Equal(expectedListCount, lists.Length);
foreach (XElement list in lists)
{
string? anchor = (string?)list.Attribute("anchor");
Assert.False(
string.IsNullOrEmpty(anchor),
$"A <list> in the {groupVisible} tab has no anchor attribute — "
+ "it will never grow with the window.");
}
}
}
/// <summary>
/// Fix round C item D3: direct pin on <see cref="AssertWithinParent"/>'s
/// height check, independent of any real markup file. A