From 3cec9df4ab3e9a2909d8e1bf26bdb91f948d9eac Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 7 Sep 2026 17:49:27 +0200 Subject: [PATCH] fix(vt): plugin draws no selection band by default in either mode Slice-7 resemblance re-check found UiMarkupList's column-less items= mode and its mode disagreed on whether a selected row gets a persistent SelectedColor fill, so a plugin's Buffs lists highlighted a row while the Monsters/Meta grids looked different for the same widget. Real VVS lists (VTank's HudList) draw no such fill at all. UiMarkupList.SelectionBandEnabled (default false) now gates the fill in both OnDraw's legacy branch and DrawColumns; MarkupDocument parses (same literal-bool convention as openupward/clearonsubmit) to opt a single list back in. selected/onchange semantics and scroll-into-view of the selected row are unchanged. Co-Authored-By: Claude Fable 5.1 --- docs/plugin-ui-markup.md | 7 +- src/AcDream.App/UI/MarkupDocument.cs | 6 + src/AcDream.App/UI/UiMarkupList.cs | 23 +++- .../UI/MarkupListColumnsTests.cs | 125 ++++++++++++++++++ 4 files changed, 158 insertions(+), 3 deletions(-) diff --git a/docs/plugin-ui-markup.md b/docs/plugin-ui-markup.md index 3f1be965..a51879c6 100644 --- a/docs/plugin-ui-markup.md +++ b/docs/plugin-ui-markup.md @@ -110,7 +110,7 @@ vanishing from the built tree. | `slider` | Horizontal scalar | `x y w h value onchange anchor` | | `field` | Single-line editable text | `x y w h text maxlength clearonsubmit onchange onsubmit color background anchor` | | `menu` | Dropdown selector | `x y w h items selected onchange rows rowheight openupward style anchor` | -| `list` | Scrollable row list (+ Slice B icon column, + Campaign VT slice 1 multi-column) | `x y w h selected onchange rowheight anchor` + either the single-column `items colors icons iconkind`, or one-to-many `` children (see "Columns" below) — never both | +| `list` | Scrollable row list (+ Slice B icon column, + Campaign VT slice 1 multi-column) | `x y w h selected onchange rowheight selectionband anchor` + either the single-column `items colors icons iconkind`, or one-to-many `` children (see "Columns" below) — never both | `menu style` is `plain` (the default) or `retail`: retail's gold pushbutton art read as an out-of-place "big yellow button" next to a plugin's own dark @@ -410,6 +410,11 @@ attributes: ``` +VVS lists draw no persistent row-selection fill, so `` matches that by +default in both the single-column and `` forms — a plugin that wants +a visible band anyway sets `selectionband="true"` (row selection itself, +including scroll-into-view, is unaffected either way). + This mirrors VTank's own Monsters tab (several boolean flag columns, a name column, and icon-button columns) — see `docs/research/vtank-kb/08-ui-views.md` §3's "Multi-column lists with typed diff --git a/src/AcDream.App/UI/MarkupDocument.cs b/src/AcDream.App/UI/MarkupDocument.cs index 00c70e15..303020b6 100644 --- a/src/AcDream.App/UI/MarkupDocument.cs +++ b/src/AcDream.App/UI/MarkupDocument.cs @@ -594,6 +594,12 @@ public static class MarkupDocument binding, "list selected"), SelectionChanged = listChanged, + // Campaign VT slice 7 resemblance re-check: VVS lists draw + // no persistent row-selection fill by default (matches + // both single-column and mode now — see + // UiMarkupList.SelectionBandEnabled). A plugin that wants + // one back opts in with . + SelectionBandEnabled = B(el, "selectionband", false), }; if (listUsesColumns) diff --git a/src/AcDream.App/UI/UiMarkupList.cs b/src/AcDream.App/UI/UiMarkupList.cs index 967a4165..ddebc0a6 100644 --- a/src/AcDream.App/UI/UiMarkupList.cs +++ b/src/AcDream.App/UI/UiMarkupList.cs @@ -73,6 +73,25 @@ public sealed class UiMarkupList : UiElement public Vector4 TextColor { get; set; } = new(0.91f, 0.87f, 0.76f, 1f); public Vector4 SelectedColor { get; set; } = new(0.28f, 0.23f, 0.08f, 0.95f); + /// + /// Campaign VT slice 7 resemblance re-check (2026-09-07): real VVS lists + /// (VTank's own HudList) draw no persistent row-selection fill at + /// all — before this fix the column-less items= mode drew + /// under the selected row while the + /// <column> mode did the same, so a plugin's Buffs lists + /// highlighted a row while the Monsters/Meta grids happened not to (or + /// vice versa, depending on which mode a given list used) — same-looking + /// widgets, inconsistent behavior. Default false now suppresses the fill + /// in BOTH 's legacy branch and + /// , matching VVS. <list + /// selectionband="true"> (parsed in 's + /// case "list") opts a single list back into a visible band for + /// plugins that want one. This gates ONLY the fill — , + /// , and the selected-row scroll-into-view + /// logic in / are unchanged. + /// + public bool SelectionBandEnabled { get; set; } + /// /// Owner live-client report 2026-09-07 ("For scrollable dropdown or the /// meta window we use the same assets as we do in for example chat or @@ -163,7 +182,7 @@ public sealed class UiMarkupList : UiElement for (int index = _topRow; index < end; index++) { float y = (index - _topRow) * RowHeight; - if (index == selected) + if (index == selected && SelectionBandEnabled) context.DrawFill(1f, y + 1f, contentWidth - 2f, RowHeight - 1f, SelectedColor); if (iconIds is not null && index < iconIds.Count && IconResolve is { } resolve) @@ -380,7 +399,7 @@ public sealed class UiMarkupList : UiElement for (int index = _topRow; index < end; index++) { float y = (index - _topRow) * RowHeight; - if (index == selected) + if (index == selected && SelectionBandEnabled) context.DrawFill(1f, y + 1f, contentWidth - 2f, RowHeight - 1f, SelectedColor); for (int c = 0; c < columns.Count; c++) diff --git a/tests/AcDream.App.Tests/UI/MarkupListColumnsTests.cs b/tests/AcDream.App.Tests/UI/MarkupListColumnsTests.cs index 81002296..0ce30736 100644 --- a/tests/AcDream.App.Tests/UI/MarkupListColumnsTests.cs +++ b/tests/AcDream.App.Tests/UI/MarkupListColumnsTests.cs @@ -1126,6 +1126,18 @@ public sealed class MarkupListColumnsTests /// entirely — i.e. against the widget's own unmodified /// OnDraw/legacy branch, not a snapshot captured from a different /// commit. Every vertex float must match exactly. + /// + /// + /// Deliberately updated for the slice-7 resemblance re-check's + /// fix: SelectedIndex + /// = 1 (a real selected row) previously meant this byte-for-byte + /// comparison implicitly included the + /// fill quad on BOTH sides. Neither the markup XML nor the hand-built + /// widget below sets selectionband/, + /// so that fill is now absent from both — the explicit assertion at the + /// end locks in that the new no-band default applies here too, rather + /// than leaving it to accidentally fall out of the byte-for-byte diff. + /// /// [Fact] public void ColumnLessList_ProducesTheIdenticalDrawRecordToTheHandBuiltWidget() @@ -1181,6 +1193,119 @@ public sealed class MarkupListColumnsTests } Assert.Null(viaMarkup.Columns); + + // Neither side opted into selectionband="true" — row 1 IS selected + // (LegacyBinding.SelectedIndex = 1) but the new default draws no + // SelectedColor fill for it, on either the markup or the hand-built + // path. + Assert.False(viaMarkup.SelectionBandEnabled); + Assert.False(handBuilt.SelectionBandEnabled); + Assert.DoesNotContain(markupVerts, s => s.Texture == 0u + && Chunk(s.Verts).Any(v => ColorMatches(v, viaMarkup.SelectedColor))); + } + + // ── Slice 7 resemblance re-check: selectionband default + opt-in ──────── + + private sealed class SelectionBandBinding + { + public IReadOnlyList Choices => new[] { "First", "Second" }; + public int Selected { get; set; } = 1; + public Action SelectIndex => _ => { }; + } + + /// Any untextured (fill) quad in whose vertex + /// color matches — used to detect the selection-band + /// fill regardless of its exact geometry. + private static bool HasFillOfColor( + IEnumerable<(uint Texture, IReadOnlyList Verts)> segs, Vector4 color) + => segs.Where(s => s.Texture == 0u) + .SelectMany(s => Chunk(s.Verts)) + .Any(v => ColorMatches(v, color)); + + /// + /// Slice-7 resemblance re-check finding: VVS lists (VTank's real + /// HudList) draw no persistent row-selection fill, so a + /// column-less <list items="..."> must match that by + /// default — defaults + /// false. selectionband="true" opts a single list back into the + /// visible band. + /// + [Theory] + [InlineData(false)] + [InlineData(true)] + public void SingleColumnList_SelectionBand_DefaultsOffAndAttributeOptsIn(bool enabled) + { + var binding = new SelectionBandBinding(); + string attr = enabled ? " selectionband=\"true\"" : ""; + string xml = + "" + + "" + + ""; + + var panel = MarkupDocument.Build(xml, binding, Sprite); + var list = Assert.IsType(panel.Children[0]); + Assert.Equal(enabled, list.SelectionBandEnabled); + + var (renderer, ctx) = MakeContext(200f, 200f); + list.DrawSelfAndChildren(ctx); + + Assert.Equal(enabled, HasFillOfColor(renderer.DebugSpriteSegmentVerts, list.SelectedColor)); + } + + /// Same fix, exercised through <column> mode + /// () — the gate applies to both + /// draw branches of the shared widget, not just the legacy one. + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ColumnModeList_SelectionBand_DefaultsOffAndAttributeOptsIn(bool enabled) + { + var binding = new SelectionBandBinding(); + string attr = enabled ? " selectionband=\"true\"" : ""; + string xml = + "" + + $"" + + " " + + ""; + + var panel = MarkupDocument.Build(xml, binding, Sprite); + var list = Assert.IsType(panel.Children[0]); + Assert.Equal(enabled, list.SelectionBandEnabled); + + var (renderer, ctx) = MakeContext(200f, 200f); + list.DrawSelfAndChildren(ctx); + + Assert.Equal(enabled, HasFillOfColor(renderer.DebugSpriteSegmentVerts, list.SelectedColor)); + } + + /// Parse-only pin: selectionband follows the same silent + /// literal-bool convention as openupward/clearonsubmit + /// ('s private B helper) — omitted + /// defaults false, and "true" sets the property, with no draw + /// involved at all. + [Fact] + public void ListSelectionBandAttribute_ParsesToProperty() + { + var binding = new SelectionBandBinding(); + const string xmlDefault = + "" + + "" + + ""; + const string xmlEnabled = + "" + + "" + + ""; + + var defaultList = Assert.IsType( + MarkupDocument.Build(xmlDefault, binding, Sprite).Children[0]); + var enabledList = Assert.IsType( + MarkupDocument.Build(xmlEnabled, binding, Sprite).Children[0]); + + Assert.False(defaultList.SelectionBandEnabled); + Assert.True(enabledList.SelectionBandEnabled); } // ── Fix round: end-to-end MarkupDocument builds (fix item 10) ───────────