# Plugin UI markup SSOT for `AcDream.Plugin.Abstractions.IUiRegistry`'s markup vocabulary — every element and attribute a plugin can put in the KSML-style XML it hands the host via `AddPanel`/`RegisterPanel`/`RegisterPanelContent`, the `{Binding}` rules those attributes follow, the DAT-icon grammar (Slice B), and the movable plugin shelf (Slice A). Both slices are recorded in [`plans/2026-09-06-plugin-shelf-and-dat-icons.md`](plans/2026-09-06-plugin-shelf-and-dat-icons.md); this page is the day-to-day reference for writing a panel, that plan is the design record. Plugins stay BCL-only: nothing in `AcDream.Plugin.Abstractions` references App/UI or Core.Items types. A plugin hands the host raw ids (spell ids, object guids, DAT indices); the host owns every texture, every composited icon, and the parser that turns markup into a live `UiElement` tree (`AcDream.App.UI.MarkupDocument`). ## Registering a panel ```csharp host.Ui.AddPanel( new PluginPanelDescriptor("main", "MossTank") { IconText = "MT", // fallback initials if IconSurfaceId is 0 IconSurfaceId = 7735, // Decal-style bare index OR a full DID — both normalize StartVisible = true, ShowInSidePanel = true, }, Path.Combine(pluginDirectory, "mosstank.xml"), binding); ``` `RegisterPanel` (same signature, returns `IDisposable`) removes the window independently of the plugin's own lifetime. `RegisterPanelContent` takes an in-memory KSML string instead of a file path — the route `AcDream.Plugins.Smoke` uses for its icon-surface proof panel (`SmokeIconPanel.cs`), 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 (`ShowInSidePanel = true`, the default). Hiding or minimizing a window never disables the plugin or pauses its `Tick`. ## The `{Binding}` rule Every attribute that isn't a plain literal is either: - a **literal** — a number, color, or string typed directly in the markup, or - a **binding** — `{PropertyName}`, resolved once at `Build` time against the binding object's public properties/`Action`/`Action` members via reflection, then **re-read every frame** through a `Func` (or invoked live for actions). A plugin updates its panel by assigning a property; it never touches `UiElement` objects directly, and never from a thread other than the one that calls `Tick`. A binding failure's severity is per-attribute, not one blanket rule — see the table below. An unrecognized `{Prop}` that resolves loudly (any row marked "Throws") throws `FormatException` **at `Build`**, the same moment any other malformed attribute throws, never silently at draw time. The attributes marked "Silent" instead fall back to something visible-but-harmless at *runtime* (the literal text, `null`, or `0`) — a plugin author who typos one of those sees a wrong-looking value on screen rather than a crash, so double check those four against the markup by eye. | Attribute(s) | On a missing/mistyped `{Prop}` | Bound CLR type | |---|---|---| | `label text`, `field text`, `menu selected`, `tooltip` (any element) | **Silent** — `BindString` falls back to the literal attribute text itself (a typo'd `{Typo}` renders as the literal string `{Typo}`) | `string` (via `.ToString()`) | | `meter cur`, `meter max` | **Silent** — `BindUint` returns `null` (the meter shows no cur/max) | `uint?` (accepts any integral type) | | `meter fill`, `slider value` | **Silent** — `BindFloat` returns `0` | `float?`/`float` | | `list items`, `menu items` | Throws | `IEnumerable` | | `list colors` | **Silent** if omitted (no color override); throws if present but mistyped | `IEnumerable` **or** `IEnumerable` (shared `BindUintList`) | | `list icons` (Slice B) | Throws if present but mistyped; omitting it entirely means no icon column at all. A negative `int` element is **silent**: it maps to `0u` (no icon for that row), matching the scalar `did`/`spell`/`item` row above | `IEnumerable` **or** `IEnumerable` | | ``/`