From c7fbbc560856c5e5814bb67f237f172bb6431732 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 7 Sep 2026 15:43:28 +0200 Subject: [PATCH] docs(ui): document resizable panels and anchor grammar Adds the "Resizable panels and anchors" section to plugin-ui-markup.md: the resizable/minw/minh attribute table for , the anchor grammar for every element that now supports it, the AnchorEdges semantics (left top default, left right / top bottom stretch, right/bottom pin- and-move), group-relative child anchoring, and one worked example (a resizable panel with a group and list that both stretch on drag). Also updates the Elements table, the "every registered window" intro paragraph, the ApplyCommon common-attributes paragraph, and the Testing conventions section to reference the new grammar and its test coverage. Co-Authored-By: Claude Fable 5.1 --- docs/plugin-ui-markup.md | 113 ++++++++++++++++++++++++++++++++------- 1 file changed, 94 insertions(+), 19 deletions(-) diff --git a/docs/plugin-ui-markup.md b/docs/plugin-ui-markup.md index c779859a..c16ecee8 100644 --- a/docs/plugin-ui-markup.md +++ b/docs/plugin-ui-markup.md @@ -36,8 +36,9 @@ in-memory KSML string instead of a file path — the route to reach for when a panel is small enough not to need its own shipped `.xml` asset. Every registered window gets a stable persisted key -(`plugin:{pluginId}:{windowId}`), drag, resize (where the markup opts in), -the global UI lock, and a button in the shared plugin shelf +(`plugin:{pluginId}:{windowId}`), drag, resize (where the markup opts in — +``, see "Resizable panels and anchors" below), the +global UI lock, and a button in the shared plugin shelf (`ShowInSidePanel = true`, the default). Hiding or minimizing a window never disables the plugin or pauses its `Tick`. @@ -98,18 +99,18 @@ vanishing from the built tree. | Element | Purpose | Key attributes | |---|---|---| -| `panel` (root) | The window itself | `x y w h title resize visible` | -| `group` | Transparent layout container | `x y w h background border visible` | -| `label` | Static or bound text | `x y text color` | -| `button` | Clickable rect + caption (+ Slice B icon) | `x y w h text color background border onclick icon iconkind` | -| `icon` | Slice B: a standalone DAT icon | `x y w h did spell item tooltip` | +| `panel` (root) | The window itself | `x y w h title resize resizable minw minh visible` | +| `group` | Transparent layout container | `x y w h background border visible anchor` | +| `label` | Static or bound text | `x y text color anchor` | +| `button` | Clickable rect + caption (+ Slice B icon) | `x y w h text color background border onclick icon iconkind anchor` | +| `icon` | Slice B: a standalone DAT icon | `x y w h did spell item tooltip anchor` | | `meter` | Retail-style nine-slice bar | `x y w h fill cur max color anchor backleft/backtile/backright frontleft/fronttile/frontright` | -| `tab` | Selectable tab button | `x y w h text selected onclick` | -| `toggle` | Lamp-style checkbox | `x y w h text checked onclick color` | -| `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 style` | -| `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 | +| `tab` | Selectable tab button | `x y w h text selected onclick anchor` | +| `toggle` | Lamp-style checkbox | `x y w h text checked onclick color anchor` | +| `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 | `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 @@ -132,12 +133,15 @@ row art, ornate scrollbar) exactly as before, unchanged. Common to every element via `ApplyCommon`: `name`/`id` (a stable control name), `visible` (literal `true`/`false` or a bound `bool` property), -`enabled` (same rule), and `tooltip` (a literal string or `{Binding}` shown +`enabled` (same rule), `tooltip` (a literal string or `{Binding}` shown through retail's own runtime tooltip popup, empty/whitespace treated as no -tooltip). The root `` is the one exception: it does **not** go -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). +tooltip), and `anchor` (which edges of the element's PARENT it keeps a fixed +margin to on resize — see "Resizable panels and anchors" below). The root +`` is the one exception: it does **not** go through `ApplyCommon` (no +`name`/`enabled`/`tooltip`/`anchor` — a top-level window is never anchored to +its own parent, only dragged/resized directly), 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). 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 @@ -154,6 +158,67 @@ the `0x` prefix to parse as hex; an all-digit string with no prefix (`did="165"`) parses as **decimal**, not hex — `did="165"` and `did="0x165"` are different ids. +## Resizable panels and anchors + +A plugin panel is **fixed-size by default** — this matches every panel +shipped before 2026-09-07 (e.g. `mosstank.xml`'s `resize="none"`). A window +opts into real user drag-resize with ``, and every +non-root element opts its OWN geometry into following that resize with +`anchor="..."`. The two attributes are independent: a resizable panel whose +children have no `anchor` just gets bigger/smaller with empty space at the +bottom-right (today's default placement, `Left|Top`); a panel with anchored +children but `resizable` left at its default `false` never actually resizes, +so the anchors never have anything to react to. + +| Attribute | Element | Meaning | +|---|---|---| +| `resizable` | `panel` (root) | `"true"` arms the window for user drag-resize on both axes (edges + corners, same mechanism chat windows use); default `false` — fixed size, exactly as before this attribute existed | +| `minw` / `minh` | `panel` (root) | The floor a drag-resize (and a persisted-layout restore) will not shrink below. Default: the panel's own authored `w`/`h` — a resizable panel never shrinks past the layout its author actually tested. Only meaningful when `resizable="true"` | +| `resize` | `panel` (root) | Pre-existing per-axis lock (`x`/`y`/`both`/`none`) that narrows `resizable="true"` to one axis; has no effect on its own now that `resizable` (default `false`) is the master switch | +| `anchor` | `group` `list` `menu` `field` `label` `button` `icon` (and `meter`/`tab`/`toggle`/`slider`) | Space-separated subset of `left top right bottom` (case-insensitive), naming which edges of the element's **direct parent** it keeps a fixed margin to as that parent resizes. Default (attribute absent) is `left top` — today's fixed placement, unchanged | + +`anchor` semantics are exactly `AcDream.App.UI.UiElement.Anchors`/ +`AnchorEdges`/`ApplyAnchor` (already used by every retail-imported window): + +- `left top` (the default) — pinned top-left at a fixed size; never stretches. +- `left right` — stretches WIDTH to track the parent (both side margins stay + fixed). +- `top bottom` — stretches HEIGHT the same way, vertically. +- `left top right bottom` — stretches on both axes. +- `right` alone (no `left`) — pins to the parent's right edge at a FIXED + width, moving as the parent resizes rather than stretching. `bottom` alone + is the same, vertically. + +An element's parent is whatever markup element directly contains it — for a +``'s children, that is the GROUP, not the panel. This is how a group +propagates resize to its own contents: give the group +`anchor="left top right bottom"` so it stretches with the panel, and give a +`` inside it `anchor="left right"` so the list stretches with the +GROUP's width in turn. An unrecognized token (a typo like +`anchor="left rihgt"`) throws `FormatException` at `Build`, naming the +offending element by its `name`/`id` — the same "malformed markup throws" +rule every other attribute in this grammar follows. + +No other markup or host wiring is needed to make a panel resizable: once +`resizable="true"` sets the window's `Resizable`/`ResizeX`/`ResizeY`/ +`MinWidth`/`MinHeight`, the SAME drag-resize, persistence (save/restore +across sessions, clamped to `minw`/`minh`), and UI-lock behavior every other +retained window already has just applies. + +```xml + + + + +``` + +Here the outer `` stretches with the panel on every edge, and the +`` inside it stretches with the GROUP on every edge in turn — dragging +the window's corner grows the whole list, not just empty panel background. + ## The icon-id grammar (Slice B) Decal/VirindiViewService plugins (the reference usage this ported: @@ -530,4 +595,14 @@ 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 `` is unaffected, and two full `MarkupDocument.Build` end-to-end tests transcribing VTank's -real Monsters- and Meta-tab column shapes. +real Monsters- and Meta-tab column shapes. `MarkupResizableAnchorTests` +covers `resizable`/`minw`/`minh` parsing, the `anchor` grammar (default, +every token combination, the unknown-token throw) across every element +listed above, live re-layout against the same recording-renderer apparatus +(a stretching list, a right-anchored button that moves, a group whose resize +propagates to its own anchored children), and a golden proving a panel with +none of these attributes draws byte-identically to itself across repeated +builds. `RetailWindowManagerTests`/`RetailWindowLayoutPersistenceTests` +cover a resizable markup panel through the real `ResizeTo`/save-restore +paths (accepts within `minw`/`minh`, a fixed panel refuses, a restored size +below the CURRENT floor clamps up to it).