From bae34aaa8b57a7476426bfa87e920b7c9d66118c Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 7 Sep 2026 19:04:56 +0200 Subject: [PATCH] =?UTF-8?q?fix(vtank):=20slice=207=20round=20F=20item=205?= =?UTF-8?q?=20=E2=80=94=20Monsters=20move-up/down=20arrows=20stay=20adjace?= =?UTF-8?q?nt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resemblance re-check: "Monsters' move-down arrow drifted ~67 px from move-up because the last (auto) column absorbed the new width — add a trailing filler column." docs/plugin-ui-markup.md's own "Width semantics" rule makes the LAST in a ALWAYS treated as auto, regardless of its own declared width — MonsterMoveDownIcons was the grid's 23rd and, until this commit, LAST column, so it silently absorbed every pixel of window growth instead of staying at its declared 23px pitch. The gap was already wrong (89.5px) at the authored 984-wide default, not just after a resize. Adds a 24th, trailing with empty items (MonsterFillerColumn) so MoveDownIcons is no longer last and keeps its own real width; the filler absorbs the remainder instead — the same "fixed icon columns + trailing auto filler" shape lstMetaRules' three 16px icon columns + two auto text columns already prove out (MarkupListColumnsTests.EndToEnd_MetaShapedSixColumnList_...). The filler's onclick (MonsterFillerClick) is a real, bound no-op — MossTankMarkupContractTests.EveryInteractiveControlDeclaresARealHandlerBinding requires every to declare SOME handler, so "no binding at all" (the other option this item's own spec offered) is not actually legal in this codebase; clicking blank filler space does nothing, but the binding itself is real. Mutation shown to fail first: the new MonstersMoveUpAndMoveDownIconsStayAdjacentAtEveryWidth theory (984 and 1100, MarkupDocument.Build over the real mosstank.xml with a real IMarkupIconResolver stub) failed at BOTH widths against the unmodified markup — 89.5px apart at 984, 147.5px at 1100 — then passed (fixed ~23px gap at both) once the filler column was added. MonstersGridHasVtanksTwentyThreeColumnsInOrderWithRetailHeaderTooltips' 23->24 column-count/type update is the other required change; VTank's own 23-column shape is unchanged, only a MossTank-only 24th filler is new. MossTank suite 724/724 (one test's expectations updated, no net count change); App markup/plugin filter 258 -> 260 (the new theory's two InlineData cases). Co-Authored-By: Claude Fable 5.1 --- src/AcDream.Plugins.MossTank/MossTankPanel.cs | 20 +++++ src/AcDream.Plugins.MossTank/mosstank.xml | 9 ++ .../MossTankMarkupBuildOverRealFilesTests.cs | 85 +++++++++++++++++++ .../MossTankMarkupContractTests.cs | 12 ++- 4 files changed, 124 insertions(+), 2 deletions(-) diff --git a/src/AcDream.Plugins.MossTank/MossTankPanel.cs b/src/AcDream.Plugins.MossTank/MossTankPanel.cs index 9ae0b7e3..696e9a54 100644 --- a/src/AcDream.Plugins.MossTank/MossTankPanel.cs +++ b/src/AcDream.Plugins.MossTank/MossTankPanel.cs @@ -1542,6 +1542,26 @@ internal sealed partial class MossTankPanel public IReadOnlyList MonsterMoveDownIcons => _monsterMoveDownIconsColumn; public Action MoveMonsterRuleUpAt => row => MoveMonsterRuleAtCore(row, -1); public Action MoveMonsterRuleDownAt => row => MoveMonsterRuleAtCore(row, 1); + // Round F item 5 (resemblance re-check: "Monsters' move-down arrow + // drifted ~67 px from move-up because the last (auto) column + // absorbed the new width — add a trailing filler column"): + // docs/plugin-ui-markup.md's own "Width semantics" rule makes the + // LAST in a ALWAYS auto regardless of its own + // declared width, so MonsterMoveDownIcons — the grid's 23rd and, at + // the time, LAST column — silently absorbed every pixel of window + // growth instead of staying at its declared 23px pitch. A trailing + // filler column (empty items) now sits after it so MoveDownIcons + // keeps its own real width and this filler absorbs the remainder + // instead — the same "fixed icon + trailing auto filler" shape + // lstMetaRules already proves out + // (EndToEnd_MetaShapedSixColumnList_AutoColumnsShareRemainder). + // MonsterFillerClick is a real, bound no-op — MossTankMarkupContractTests' + // EveryInteractiveControlDeclaresARealHandlerBinding requires every + // interactive control (every included) to declare SOME + // handler; clicking blank filler space does nothing, but the binding + // itself is real, not omitted. + public IReadOnlyList MonsterFillerColumn => Array.Empty(); + public Action MonsterFillerClick => static _ => { }; /// Vitals line, using the same numbers the character panel shows. public string Vitals => _vitals; diff --git a/src/AcDream.Plugins.MossTank/mosstank.xml b/src/AcDream.Plugins.MossTank/mosstank.xml index b31c8546..7387a9c4 100644 --- a/src/AcDream.Plugins.MossTank/mosstank.xml +++ b/src/AcDream.Plugins.MossTank/mosstank.xml @@ -459,6 +459,15 @@ + + + /// Round F item 5 (resemblance re-check: "Monsters' move-down arrow + /// drifted ~67 px from move-up because the last (auto) column + /// absorbed the new width — add a trailing filler column"). + /// MonsterMoveDownIcons used to be the grid's LAST <column>, + /// and docs/plugin-ui-markup.md's own "Width semantics" rule makes + /// the LAST column ALWAYS auto regardless of its own declared width + /// — so it silently absorbed every pixel of window growth instead of + /// staying at its declared 23px pitch, dragging the down-arrow away + /// from the up-arrow as the panel widened. Proves the two icons stay + /// the same fixed ~23px apart at both the authored default (984) and + /// the D-6 enlarged size (1100), through the SAME real mosstank.xml + /// file and MossTankPanel this file's other real-file tests use. + /// + [Theory] + [InlineData(984f)] + [InlineData(1100f)] + public void MonstersMoveUpAndMoveDownIconsStayAdjacentAtEveryWidth(float width) + { + string xml = File.ReadAllText( + Path.Combine(MossTankMarkupDirectory, "mosstank.xml")); + var panel = new MossTankPanel(new StubHost()); + + // The MoveUp/MoveDown icon columns need a real IMarkupIconResolver + // wired (unlike every other test in this file, which only cares + // about geometry) — MarkupDocument.Build leaves + // silently undrawn without one, so an identity stub is required + // here to actually exercise DrawIconCell. + UiNineSlicePanel built = MarkupDocument.Build( + xml, panel, static id => (id, 32, 32), icons: new IdentityIconResolver()); + + UiPanel[] tabGroups = built.Children + .Where(static child => child.GetType() == typeof(UiPanel)) + .Cast() + .ToArray(); + Assert.Equal(9, tabGroups.Length); + foreach (UiPanel group in tabGroups) + group.Visible = false; + UiPanel monstersGroup = tabGroups[3]; + monstersGroup.Visible = true; + + 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 default captures every anchored + // descendant's baseline margins; only THEN resize to the target + // width and redraw — the same two-draw shape every other + // real-file re-layout test in this file uses. Both draws record + // sprite calls into the SAME device, so each icon's quad appears + // TWICE (once per draw) — .Last() reads the final, resized-layout + // draw, not the stale authored-default one. + built.DrawSelfAndChildren(ctx); + built.Width = width; + built.DrawSelfAndChildren(ctx); + + var moveUpQuad = renderer.DebugSpriteSegmentVerts + .Last(static s => s.Texture == 0x060028FCu); + var moveDownQuad = renderer.DebugSpriteSegmentVerts + .Last(static s => s.Texture == 0x060028FDu); + + float gap = moveDownQuad.Verts[0] - moveUpQuad.Verts[0]; + Assert.True( + gap is >= 20f and <= 26f, + $"MoveUp/MoveDown icons are {gap}px apart at width {width} — " + + "expected VTank's fixed ~23px icon pitch, not the growing " + + "gap a still-last, still-auto MoveDown column would produce."); + } + private static void AssertResolvedNoSiblingOverlap(UiElement container) { // Same "invisible subtree never got a real anchor pass" reasoning @@ -351,6 +421,21 @@ public sealed class MossTankMarkupBuildOverRealFilesTests public IGpuFrame? CurrentFrame => null; } + /// Identity for icon-column + /// draw tests — every non-zero id "resolves" to itself as the texture + /// (16x16), matching MarkupListColumnsTests.FakeIconResolver's + /// own convention so a real DID like the Monsters grid's move-up/ + /// move-down pair (0x060028FC/FD) round-trips as its own texture id. + private sealed class IdentityIconResolver : IMarkupIconResolver + { + public (uint tex, int w, int h) ResolveDid(uint did) => + did == 0u ? (0u, 0, 0) : (did, 16, 16); + public (uint tex, int w, int h) ResolveSpell(uint spellId) => + spellId == 0u ? (0u, 0, 0) : (spellId, 16, 16); + public (uint tex, int w, int h) ResolveItem(uint objectId) => + objectId == 0u ? (0u, 0, 0) : (objectId, 16, 16); + } + private sealed class StubHost : IPluginHost { public bool HasUi => false; diff --git a/tests/AcDream.Plugins.MossTank.Tests/MossTankMarkupContractTests.cs b/tests/AcDream.Plugins.MossTank.Tests/MossTankMarkupContractTests.cs index 4ae496b9..5e1aa3b2 100644 --- a/tests/AcDream.Plugins.MossTank.Tests/MossTankMarkupContractTests.cs +++ b/tests/AcDream.Plugins.MossTank.Tests/MossTankMarkupContractTests.cs @@ -209,6 +209,14 @@ public sealed class MossTankMarkupContractTests // docs/research/vtank-kb/08-ui-views.md §1 "Tab: Monsters" (column // type/order) and the decompile's own tooltip strings // (refs/vtank/decompiled/uTank2/PluginCore.cs:1693-1707). + // + // Round F item 5 appended a 24th column: a genuinely inert + // trailing filler (empty items, no onclick) so the icon column + // before it (MoveDownIcons) is no longer the grid's LAST column + // and keeps its own declared 23px width instead of silently + // absorbing every pixel of window growth (docs/plugin-ui-markup.md's + // "Width semantics" — the last column is ALWAYS auto). It has no + // VTank counterpart; VTank's own 23-column shape is unchanged. XDocument document = XDocument.Load( Path.Combine(AppContext.BaseDirectory, "mosstank.xml")); XElement root = Assert.IsType(document.Root); @@ -217,14 +225,14 @@ public sealed class MossTankMarkupContractTests XElement list = Assert.Single(monstersGroup.Elements("list")); XElement[] columns = list.Elements("column").ToArray(); - Assert.Equal(23, columns.Length); + Assert.Equal(24, columns.Length); string[] expectedTypes = [ "check", "check", "check", "check", "check", "check", "check", "check", "check", "check", "check", "check", "check", "check", "text", "text", "text", "text", "text", "text", "text", - "icon", "icon", + "icon", "icon", "text", ]; Assert.Equal(expectedTypes, columns.Select(c => (string?)c.Attribute("type")));