diff --git a/docs/plugin-ui-markup.md b/docs/plugin-ui-markup.md index cf711a5f..ee601902 100644 --- a/docs/plugin-ui-markup.md +++ b/docs/plugin-ui-markup.md @@ -78,6 +78,15 @@ check those four against the markup by eye. | `slider onchange` | Throws | `Action` | | `field onchange`, `field onsubmit`, `menu onchange` | Throws | `Action` | | `list onchange` | Throws | `Action` | +| `column items` (`type="text"`) | Throws — REQUIRED, unlike the single-column list's own `items` sugar it mirrors | `IReadOnlyList` | +| `column colors` (`type="text"`) | **Silent** if omitted (no per-row override, same rule as `list colors`); throws if present but mistyped | `IReadOnlyList` **or** `IReadOnlyList` | +| `column onclick` (`type="text"`) | **Silent** if omitted (keeps the original select-the-row behavior); throws if present but mistyped | `Action` (row index) | +| `column values` (`type="check"`) | Throws — REQUIRED (there is no "no check column" fallback the way `list icons` has "no icon column") | `IReadOnlyList` | +| `column values` (`type="icon"`) | Throws — REQUIRED | `IReadOnlyList` **or** `IReadOnlyList` | +| `column onchange` (`type="check"`) | Throws — REQUIRED (unlike the list's own optional `onchange`) | `Action` (row index) | +| `column onclick` (`type="icon"`) | Throws — REQUIRED | `Action` (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 `` 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:** `` 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 `` +with `` children is no longer limited to one padded text column. A +`` with no `` 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 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 `` matches this by letting a `` declare +`` children instead of the single-column `items`/`colors`/`icons` +attributes: + +```xml + + + + + +``` + +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. + +### `` 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 `` vs. every other column | +| `items` | `type="text"` | yes | `{IReadOnlyList}` — one row of text per index | +| `colors` | `type="text"` | no | `{IReadOnlyList}`, `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}` — 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}` — the checked state per row | +| `onchange` | `type="check"` | yes | `{Action}` — 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}` (or `IReadOnlyList`) — 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 `` above — one kind per column, not per row | +| `onclick` | `type="icon"` | yes | `{Action}` — 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 `` with an unrecognized `type`, +a missing required binding for its type, or any `` child element that +isn't `` at all, throws `FormatException` at `Build`. A `` with +`` children cannot ALSO use the single-column `items`/`colors`/`icons` +attributes on the `` 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 `` 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 `