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) ───────────