fix(vt): list column fix round 11/11 — docs for the whole fix round

docs/plugin-ui-markup.md's Columns section is rewritten to match every
production change from items 1-9:

- New "Width semantics" section: width="*", the last-column-always-auto
  rule (and how it combines with an earlier explicit "*"), the
  non-last-column Build-time throw, and the runtime overflow clamp.
- The <column> attribute grammar table gains column onclick
  (type="text", optional, Action<int>) and points width at the new
  section instead of the retired "last column ignores its own width"
  one-liner.
- New "Short columns past their own row count" section: text/icon draw
  nothing, check draws unchecked.
- Check-column glyph section updated for the shared UiCheckLamp
  primitive and its horizontal centering.
- New "The PITCH convention for transcribing a VTank column table"
  section: VVS's WPaddingOuter=3px/WPadding=7px/16px scrollbar reserve/
  forced-13px-check-columns have no direct acdream equivalent, so
  transcribing a real mainView.xml column table means declaring each
  column's PITCH (fixedwidth+7, or 13+7 for a check column regardless
  of its real fixedwidth) and reserving 16px on the last column.
- The worked example now uses rowheight="18" (VVS's own Padding*2+
  ControlHeight pitch, already the widget's default) and PITCH-computed
  widths (20 for a check column, 127 for a 120px text column) instead
  of arbitrary numbers, with a text onclick and a width="*" icon
  column added to demonstrate both new attributes in place.
- The bindings truth table gains column onclick (type="text") and both
  column width throw/silent rows (non-last vs. the list's last column).
- Testing conventions paragraph updated to mention width semantics, the
  row-bound click guard, and the two end-to-end tests.

Verification: dotnet build AcDream.slnx -c Release (green), dotnet test
tests/AcDream.App.Tests -c Release --filter
"FullyQualifiedName~Markup|FullyQualifiedName~UiMarkupList|FullyQualifiedName~PluginSidePanel"
(130/130), dotnet test tests/AcDream.Plugins.MossTank.Tests -c Release
--filter "FullyQualifiedName~Markup" (9/9) — closes the fix round.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-06 21:31:58 +02:00
parent e178c6abac
commit 026b512074

View file

@ -80,10 +80,13 @@ check those four against the markup by eye.
| `list onchange` | Throws | `Action<int>` |
| `column items` (`type="text"`) | Throws — REQUIRED, unlike the single-column list's own `items` sugar it mirrors | `IReadOnlyList<string>` |
| `column colors` (`type="text"`) | **Silent** if omitted (no per-row override, same rule as `list colors`); throws if present but mistyped | `IReadOnlyList<uint>` **or** `IReadOnlyList<int>` |
| `column onclick` (`type="text"`) | **Silent** if omitted (keeps the original select-the-row behavior); throws if present but mistyped | `Action<int>` (row index) |
| `column values` (`type="check"`) | Throws — REQUIRED (there is no "no check column" fallback the way `list icons` has "no icon column") | `IReadOnlyList<bool>` |
| `column values` (`type="icon"`) | Throws — REQUIRED | `IReadOnlyList<uint>` **or** `IReadOnlyList<int>` |
| `column onchange` (`type="check"`) | Throws — REQUIRED (unlike the list's own optional `onchange`) | `Action<int>` (row index) |
| `column onclick` (`type="icon"`) | Throws — REQUIRED | `Action<int>` (row index) |
| `column width` (any type, NOT the list's last column) | Throws if missing, unparseable, or `<= 0` — UNLESS it is the literal `"*"` | `float`, or the literal `"*"` for auto |
| `column width` (the list's LAST column) | **Silent** — never validated, never used for layout (it always absorbs the remainder), UNLESS it is the literal `"*"` (then it joins the auto-sharing group instead of taking 100% of the remainder alone) | `float`, or the literal `"*"` |
The icon-id row is the one binding here whose failure mode depends on WHEN you look: a typo'd property name is caught immediately at `Build`, but a property that exists yet holds the wrong kind of value at runtime is only ever discovered later, from inside a live draw.
@ -308,27 +311,33 @@ col)`. acdream's `<list>` matches this by letting a `<list>` declare
attributes:
```xml
<list x="8" y="24" w="256" h="120" rowheight="17"
<list x="8" y="24" w="256" h="120" rowheight="18"
selected="{SelectedMonster}" onchange="{SelectMonster}">
<column type="text" width="140" items="{MonsterNames}"/>
<column type="check" width="24" values="{MonsterFester}" onchange="{ToggleFester}"/>
<column type="icon" width="24" iconkind="did" values="{MonsterIcons}" onclick="{PingMonster}"/>
<column type="check" width="20" values="{MonsterFester}" onchange="{ToggleFester}"/>
<column type="text" width="127" items="{MonsterNames}" onclick="{PingMonster}"/>
<column type="icon" width="*" iconkind="did" values="{MonsterIcons}" onclick="{MoveMonsterUp}"/>
</list>
```
This mirrors VTank's own Monsters tab (a name column plus several boolean
flag columns plus an icon-button column) — see
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
columns" gap and its proposed extension, which this implements verbatim.
columns" gap and its proposed extension, which this implements verbatim. The
`rowheight="18"` above is the widget's own default (`UiMarkupList.RowHeight`),
chosen to match VVS's own row pitch exactly: `Padding*2 + ControlHeight` =
`1*2 + 16` = `18` (`docs/research/vtank-kb/08-ui-views.md`'s `HudList` row:
`Padding=1px`, `ControlHeight=16px`) — an author who omits `rowheight`
entirely already gets VVS's pitch for free.
### `<column>` attribute grammar
| Attribute | Applies to | Required | Meaning |
|---|---|---|---|
| `type` | every column | yes | `text`, `check`, or `icon` — any other value throws `FormatException` at `Build` |
| `width` | every column | no (defaults to `0`) | Column width in px. **The LAST column in a `<list>` ignores its own declared width and always absorbs whatever room remains** after every earlier column — recomputed every frame off the list's live width, so a resizable list re-flows its last column like every other retained widget |
| `width` | every column | see below | Column width in px, or `"*"` for auto. See "Width semantics" below — the rules differ for the LAST column in a `<list>` vs. every other column |
| `items` | `type="text"` | yes | `{IReadOnlyList<string>}` — one row of text per index |
| `colors` | `type="text"` | no | `{IReadOnlyList<uint>}`, `0xRRGGBB` per row (same grammar as the single-column list's own `colors`); omitted rows (or the whole attribute) fall back to the list's `TextColor` |
| `onclick` | `type="text"` | no | `{Action<int>}` — fired with the ROW INDEX on a click anywhere in the cell INSTEAD of selecting the row. Omitted (the default) keeps the original select-the-row behavior; present but malformed throws `FormatException` at `Build`. None of VTank's eight lists actually relies on row selection — every real text cell in `mainView.xml` is wired as an action target — so a new column is usually written WITH an `onclick` |
| `values` | `type="check"` | yes | `{IReadOnlyList<bool>}` — the checked state per row |
| `onchange` | `type="check"` | yes | `{Action<int>}` — fired with the ROW INDEX on a click anywhere in the cell; the plugin flips its own bool, the column never mutates `values`' backing collection itself |
| `values` | `type="icon"` | yes | `{IReadOnlyList<uint>}` (or `IReadOnlyList<int>`) — one icon id per row, same id-space rules as `list icons` |
@ -342,20 +351,62 @@ error, not a silently-inert control. A `<column>` with an unrecognized `type`,
a missing required binding for its type, or any `<list>` child element that
isn't `<column>` at all, throws `FormatException` at `Build`. A `<list>` with
`<column>` children cannot ALSO use the single-column `items`/`colors`/`icons`
attributes on the `<list>` element itself — pick one form per list.
attributes on the `<list>` element itself — pick one form per list. Every
column-attribute throw message identifies the offending column by position
and declared type — `column[2] type="check" values`, not just `"column
values"` — so a list with several columns of the same type still points at
the right one.
### Width semantics
`width="*"` means AUTO: this column shares the list's remaining width
EQUALLY with every other auto column, VVS's own "0-width column auto-sizes"
rule (`docs/research/vtank-kb/08-ui-views.md`'s `HudList` row: "a 0-width
text/button/edit/list/fixedlayout/notebook column auto-sizes... share the
remaining width equally"). `width="*"` is legal on ANY column, including the
last.
The LAST column in a `<list>` is special: it is ALWAYS treated as auto —
sharing the remaining width like every other auto column when one or more
earlier columns also declare `width="*"`, or absorbing 100% of the remainder
by itself when no other column does (the original, still-default behavior).
Its own declared `width` (or omitting `width` entirely) is never validated
and never used for layout — only an explicit `width="*"` on the last column
actually changes anything (it makes the last column share evenly with
earlier auto columns instead of taking the whole remainder alone). When two
or more columns end up sharing, integer-division remainder goes to the LAST
one — e.g. three columns sharing 100px split 33/33/34, not 33/33/33 with 1px
unaccounted for.
Every OTHER (non-last) column's declared `width` is validated at `Build`: **a
missing, unparseable, or non-positive `width` throws `FormatException`**
naming the column's index and declared type (`column[0] type="text" width
must be a positive number or "*", got (missing)`) — UNLESS it is `width="*"`.
At layout time (recomputed every frame off the list's live width, so a
resizable list re-flows like every other retained widget), a declared width
that would overflow the list's total width is CLAMPED to whatever room is
actually left, walked left to right — every column after the overflow point
gets `0` width and draws nothing (a `<=0`-width cell is skipped entirely, the
same as today).
### Row count, selection, and clicks
Row count is the longest bound column (a text column with 20 rows next to a
check column with only 5 simply draws 15 rows of empty checkboxes — short
columns never truncate the whole list). The list's own `selected`/`onchange`
attributes keep exactly their single-column meaning: a click in a **text**
column selects that row (and fires the list's `onchange` with the row index,
same as today). A click in a **check** or **icon** column instead fires that
check column with only 5 simply draws 15 rows past its own data — see "Short
columns past their own row count" below for what each column kind does
there). The list's own `selected`/`onchange` attributes keep exactly their
single-column meaning: a click in a **text** column without its own
`onclick` selects that row (and fires the list's `onchange` with the row
index, same as today). A click in a **check** or **icon** column, or a
**text** column that DOES declare its own `onclick`, instead fires that
column's own `onchange`/`onclick` and does **not** change the list's
selection — VVS's per-cell `Click(row, col)` folded into a per-column
callback, since acdream's binding model is per-attribute rather than
per-cell. Scrolling works exactly as the single-column list already does.
per-cell. A click landing on a row past that SPECIFIC column's own bound
data (even though the row is valid for the list overall, because some OTHER
column has more rows) fires nothing — no callback, no crash. Scrolling works
exactly as the single-column list already does.
### No header row
@ -369,9 +420,44 @@ is no dedicated header markup to learn.
### Check-column glyph
A `type="check"` cell draws with the exact same five-band lamp glyph as
`<toggle>` (`UiMarkupToggle`'s checked/unchecked colors and `DrawLamp`
primitive), so a column checkbox reads identically to every other checkbox
in the client rather than a bespoke box-and-tick.
`<toggle>` (both now share one `UiCheckLamp` primitive — its checked/
unchecked colors and `Draw` method), so a column checkbox reads identically
to every other checkbox in the client rather than a bespoke box-and-tick.
The glyph is centered horizontally in its cell (matching how an icon cell
already centers its sprite) — a check column is routinely declared wider
than the glyph itself under the PITCH convention below.
### Short columns past their own row count
A **text** or **icon** cell past its own column's row count draws nothing —
there is no sensible default string or icon to show. A **check** cell past
its own column's row count still draws the lamp, UNCHECKED — VVS
materializes every cell in a row regardless of which columns actually have
data for it, and there is always a sensible default for a boolean (false).
### The PITCH convention for transcribing a VTank column table
VVS's own `HudList` reserves geometry acdream's column model doesn't have a
separate concept for: `WPaddingOuter=3px` (the list's own left/right
margin), `WPadding=7px` (a gap BETWEEN columns), and a themed
`VScrollBarButtonSize=16px` (scrollbar width, reserved on the right). It
also forces every `CheckColumn` to a fixed 13px regardless of its declared
`fixedwidth`. acdream's column model has no separate gap/scrollbar/
forced-width concept — every column's declared `width` is its full cell
width, columns sit directly adjacent with no gap, and a check column uses
whatever `width` it's given like any other column.
To transcribe a real VTank column table (as in
`refs/vtank/uTank2.ViewXML.mainView.xml`) faithfully, declare each column's
**PITCH** instead of its raw `fixedwidth`: `pitch = fixedwidth + 7` (baking
VVS's inter-column `WPadding` into the cell width itself, since acdream has
no separate gap). For a `CheckColumn`, use VVS's forced 13px as the
`fixedwidth` regardless of whatever `fixedwidth` the source XML declares
(`16 -> 13 + 7 = 20`, not `16 + 7 = 23`). Reserve VVS's 16px scrollbar width
on the LAST column specifically (add it to that column's own pitch, or fold
it into the list's total declared `w`) — acdream's list draws no scrollbar
of its own today, but reserving the space keeps the transcribed proportions
matching what a real VVS `HudList` would show once one exists.
### Backward compatibility
@ -417,7 +503,12 @@ in-test class recording which id/kind it was asked to resolve) rather than a
live DAT — see `tests/AcDream.App.Tests/UI/`. `PluginSidePanelTests` exercises
the shelf's drag/collapse/hide/persistence behavior against a bare `UiRoot`.
`MarkupListColumnsTests` covers the Columns extension above: per-column
binding-type validation, draw-level column-offset/clipping/check-glyph pins
against the same recording-renderer apparatus, hit-test routing (text
selects; check/icon fire their own callback and never touch selection), and
a backward-compatibility proof that a column-less `<list>` is unaffected.
binding-type validation (every throw naming its column by index and type),
width semantics (`"*"` sharing, the last-column-always-auto rule, the
overflow clamp), the per-column row-bound click guard, draw-level
column-offset/clipping/check-glyph pins against the same recording-renderer
apparatus, hit-test routing (text selects unless it has its own `onclick`;
check/icon/onclick-text fire their own callback and never touch selection),
a backward-compatibility proof that a column-less `<list>` is unaffected,
and two full `MarkupDocument.Build` end-to-end tests transcribing VTank's
real Monsters- and Meta-tab column shapes.