merge(vt): plugin <list> draws no selection band by default (selectionband="true" opts in)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-07 17:49:59 +02:00
commit 7b7e5e579a
4 changed files with 158 additions and 3 deletions

View file

@ -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 `<column>` 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 `<column>` 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:
</list>
```
VVS lists draw no persistent row-selection fill, so `<list>` matches that by
default in both the single-column and `<column>` 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

View file

@ -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 <column> mode now — see
// UiMarkupList.SelectionBandEnabled). A plugin that wants
// one back opts in with <list selectionband="true">.
SelectionBandEnabled = B(el, "selectionband", false),
};
if (listUsesColumns)

View file

@ -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);
/// <summary>
/// Campaign VT slice 7 resemblance re-check (2026-09-07): real VVS lists
/// (VTank's own <c>HudList</c>) draw no persistent row-selection fill at
/// all — before this fix the column-less <c>items=</c> mode drew
/// <see cref="SelectedColor"/> under the selected row while the
/// <c>&lt;column&gt;</c> 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 <see cref="OnDraw"/>'s legacy branch and
/// <see cref="DrawColumns"/>, matching VVS. <c>&lt;list
/// selectionband="true"&gt;</c> (parsed in <see cref="MarkupDocument"/>'s
/// <c>case "list"</c>) opts a single list back into a visible band for
/// plugins that want one. This gates ONLY the fill — <see cref="SelectedIndexSource"/>,
/// <see cref="SelectionChanged"/>, and the selected-row scroll-into-view
/// logic in <see cref="OnDraw"/>/<see cref="DrawColumns"/> are unchanged.
/// </summary>
public bool SelectionBandEnabled { get; set; }
/// <summary>
/// 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++)

View file

@ -1126,6 +1126,18 @@ public sealed class MarkupListColumnsTests
/// entirely — i.e. against the widget's own unmodified
/// <c>OnDraw</c>/legacy branch, not a snapshot captured from a different
/// commit. Every vertex float must match exactly.
///
/// <para>
/// Deliberately updated for the slice-7 resemblance re-check's
/// <see cref="UiMarkupList.SelectionBandEnabled"/> fix: <c>SelectedIndex
/// = 1</c> (a real selected row) previously meant this byte-for-byte
/// comparison implicitly included the <see cref="UiMarkupList.SelectedColor"/>
/// fill quad on BOTH sides. Neither the markup XML nor the hand-built
/// widget below sets <c>selectionband</c>/<see cref="UiMarkupList.SelectionBandEnabled"/>,
/// 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.
/// </para>
/// </summary>
[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<string> Choices => new[] { "First", "Second" };
public int Selected { get; set; } = 1;
public Action<int> SelectIndex => _ => { };
}
/// <summary>Any untextured (fill) quad in <paramref name="segs"/> whose vertex
/// color matches <paramref name="color"/> — used to detect the selection-band
/// fill regardless of its exact geometry.</summary>
private static bool HasFillOfColor(
IEnumerable<(uint Texture, IReadOnlyList<float> Verts)> segs, Vector4 color)
=> segs.Where(s => s.Texture == 0u)
.SelectMany(s => Chunk(s.Verts))
.Any(v => ColorMatches(v, color));
/// <summary>
/// Slice-7 resemblance re-check finding: VVS lists (VTank's real
/// <c>HudList</c>) draw no persistent row-selection fill, so a
/// column-less <c>&lt;list items="..."&gt;</c> must match that by
/// default — <see cref="UiMarkupList.SelectionBandEnabled"/> defaults
/// false. <c>selectionband="true"</c> opts a single list back into the
/// visible <see cref="UiMarkupList.SelectedColor"/> band.
/// </summary>
[Theory]
[InlineData(false)]
[InlineData(true)]
public void SingleColumnList_SelectionBand_DefaultsOffAndAttributeOptsIn(bool enabled)
{
var binding = new SelectionBandBinding();
string attr = enabled ? " selectionband=\"true\"" : "";
string xml =
"<panel x=\"0\" y=\"0\" w=\"200\" h=\"100\">" +
"<list x=\"0\" y=\"0\" w=\"180\" h=\"60\" rowheight=\"18\" " +
$"items=\"{{Choices}}\" selected=\"{{Selected}}\" onchange=\"{{SelectIndex}}\"{attr}/>" +
"</panel>";
var panel = MarkupDocument.Build(xml, binding, Sprite);
var list = Assert.IsType<UiMarkupList>(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));
}
/// <summary>Same fix, exercised through <c>&lt;column&gt;</c> mode
/// (<see cref="UiMarkupList.DrawColumns"/>) — the gate applies to both
/// draw branches of the shared widget, not just the legacy one.</summary>
[Theory]
[InlineData(false)]
[InlineData(true)]
public void ColumnModeList_SelectionBand_DefaultsOffAndAttributeOptsIn(bool enabled)
{
var binding = new SelectionBandBinding();
string attr = enabled ? " selectionband=\"true\"" : "";
string xml =
"<panel x=\"0\" y=\"0\" w=\"200\" h=\"100\">" +
$"<list x=\"0\" y=\"0\" w=\"180\" h=\"60\" rowheight=\"18\" selected=\"{{Selected}}\" onchange=\"{{SelectIndex}}\"{attr}>" +
" <column type=\"text\" width=\"*\" items=\"{Choices}\"/>" +
"</list></panel>";
var panel = MarkupDocument.Build(xml, binding, Sprite);
var list = Assert.IsType<UiMarkupList>(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));
}
/// <summary>Parse-only pin: <c>selectionband</c> follows the same silent
/// literal-bool convention as <c>openupward</c>/<c>clearonsubmit</c>
/// (<see cref="MarkupDocument"/>'s private <c>B</c> helper) — omitted
/// defaults false, and <c>"true"</c> sets the property, with no draw
/// involved at all.</summary>
[Fact]
public void ListSelectionBandAttribute_ParsesToProperty()
{
var binding = new SelectionBandBinding();
const string xmlDefault =
"<panel x=\"0\" y=\"0\" w=\"200\" h=\"100\">" +
"<list x=\"0\" y=\"0\" w=\"180\" h=\"60\" items=\"{Choices}\" " +
"selected=\"{Selected}\" onchange=\"{SelectIndex}\"/>" +
"</panel>";
const string xmlEnabled =
"<panel x=\"0\" y=\"0\" w=\"200\" h=\"100\">" +
"<list x=\"0\" y=\"0\" w=\"180\" h=\"60\" items=\"{Choices}\" " +
"selected=\"{Selected}\" onchange=\"{SelectIndex}\" selectionband=\"true\"/>" +
"</panel>";
var defaultList = Assert.IsType<UiMarkupList>(
MarkupDocument.Build(xmlDefault, binding, Sprite).Children[0]);
var enabledList = Assert.IsType<UiMarkupList>(
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) ───────────