merge(vt): slice 1 Part B — multi-column <list> markup (review-closed)
Campaign VT slice 1 Part B: <list><column type=text|check|icon> with per-column bindings, text onclick, width="*" auto share, row-bound callback guards, shared UiCheckLamp, Meta/Monsters-shaped end-to-end tests. Two Opus lenses + fix round + narrow re-review: MERGE-READY. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
commit
da0fcb3741
9 changed files with 2543 additions and 89 deletions
|
|
@ -78,6 +78,15 @@ check those four against the markup by eye.
|
|||
| `slider onchange` | Throws | `Action<float>` |
|
||||
| `field onchange`, `field onsubmit`, `menu onchange` | Throws | `Action<string>` |
|
||||
| `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.
|
||||
|
||||
|
|
@ -100,7 +109,7 @@ vanishing from the built tree.
|
|||
| `slider` | Horizontal scalar | `x y w h value onchange` |
|
||||
| `field` | Single-line editable text | `x y w h text maxlength clearonsubmit onchange onsubmit color background` |
|
||||
| `menu` | Dropdown selector | `x y w h items selected onchange rows rowheight openupward` |
|
||||
| `list` | Scrollable row list (+ Slice B icon column) | `x y w h items colors selected onchange rowheight icons iconkind` |
|
||||
| `list` | Scrollable row list (+ Slice B icon column, + Campaign VT slice 1 multi-column) | `x y w h selected onchange rowheight` + either the single-column `items colors icons iconkind`, or one-to-many `<column>` children (see "Columns" below) — never both |
|
||||
|
||||
Common to every element via `ApplyCommon`: `name`/`id` (a stable control
|
||||
name), `visible` (literal `true`/`false` or a bound `bool` property),
|
||||
|
|
@ -111,10 +120,11 @@ through `ApplyCommon` (no `name`/`enabled`/`tooltip`), and its `visible`
|
|||
attribute accepts a `{Binding}` only — a literal `visible="true"` on the
|
||||
root is not parsed (unlike every child element, where a literal is fine).
|
||||
|
||||
**LIMITATION:** `<list>` has exactly one text column (plus the optional
|
||||
Slice B icon column) — there is no multi-column list yet. A plugin that
|
||||
needs tabular rows today pads its own fixed-width text (`$"{name,-16}{value,6}"`).
|
||||
Real multi-column support is deferred to the MossTank plugin work.
|
||||
Multi-column lists are real (Campaign VT slice 1 Part B, below) — a `<list>`
|
||||
with `<column>` children is no longer limited to one padded text column. A
|
||||
`<list>` with no `<column>` children stays exactly the older single-column
|
||||
form (`items`/`colors`/`icons`/`iconkind` on the element itself); the two
|
||||
forms are mutually exclusive on one element.
|
||||
|
||||
`list colors`' values are `0xRRGGBB` (opaque, no alpha channel), while every
|
||||
`color=`/`background=`/`border=` attribute elsewhere is `#AARRGGBB` (alpha
|
||||
|
|
@ -291,6 +301,171 @@ public IEnumerable<string> SpellRows =>
|
|||
tile, no composited badge — uses `iconkind="did"` instead, with `icons`
|
||||
yielding `IconId` rather than `SpellId`.)
|
||||
|
||||
## Columns (Campaign VT slice 1 Part B)
|
||||
|
||||
VVS's `HudList` (the VirindiViewService list control VTank's own `mainView.xml`
|
||||
uses) supports N independently-typed columns per row — text, checkbox, and
|
||||
icon cells side by side in one scrolling grid, each with its own `Click(row,
|
||||
col)`. acdream's `<list>` matches this by letting a `<list>` declare
|
||||
`<column>` children instead of the single-column `items`/`colors`/`icons`
|
||||
attributes:
|
||||
|
||||
```xml
|
||||
<list x="8" y="24" w="256" h="120" rowheight="18"
|
||||
selected="{SelectedMonster}" onchange="{SelectMonster}">
|
||||
<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 (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. 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 | 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` |
|
||||
| `iconkind` | `type="icon"` | no (defaults `"did"`) | `did`/`spell`/`item`, same three-source dispatch as `<list icons iconkind>` above — one kind per column, not per row |
|
||||
| `onclick` | `type="icon"` | yes | `{Action<int>}` — fired with the ROW INDEX on a click anywhere in the cell |
|
||||
|
||||
Unlike the single-column list's optional `icons`/`onchange`, a `check`/`icon`
|
||||
column's own `values` and `onchange`/`onclick` are **required** — a column
|
||||
that can never fire anything, or has nothing to draw, is a Build-time author
|
||||
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. 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 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. 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
|
||||
|
||||
VVS's `HudList` has no built-in header row either — the column-caption
|
||||
glyphs seen in VTank's own `mainView.xml` (e.g. the Monsters tab's single-letter
|
||||
"F"/"B"/"G"/"I"/… flag headers) are ordinary `StaticText` controls placed
|
||||
manually above the list. acdream matches this for free: put a `<label>` (or
|
||||
several, one per column, hand-positioned) directly above the `<list>` — there
|
||||
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>` (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
|
||||
|
||||
A `<list>` with no `<column>` children is byte-for-byte the original
|
||||
single-text-column widget — every existing panel (including every current
|
||||
MossTank tab) keeps working unchanged; `<column>` is additive, not a
|
||||
migration.
|
||||
|
||||
## The plugin shelf (Slice A)
|
||||
|
||||
The shelf (`AcDream.App.UI.PluginSidePanel`) is the right-edge strip of
|
||||
|
|
@ -327,3 +502,13 @@ like every other window.
|
|||
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 (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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue