# 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 that resolves to the wrong CLR type, or names a property that doesn't exist, throws `FormatException` **at `Build`** — the same moment any other malformed attribute throws — never silently at draw time. A resolved binding that returns an out-of-range or default value (0, empty, null) at *runtime* draws nothing/looks empty; it never throws after the panel has loaded. ## Elements | 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` | | `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` | | `list` | Scrollable row list (+ Slice B icon column) | `x y w h items colors selected onchange rowheight icons iconkind` | 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 through retail's own runtime tooltip popup). ## The icon-id grammar (Slice B) Decal/VirindiViewService plugins (the reference usage this ported: MosswartMassacre's `HudPictureBox.Image` assignments, fed from Decal's `FileService.SpellTable`/`SkillTable` icon columns) hand out **bare portal.dat indices** — small integers, not full `0x06xxxxxx` RenderSurface DIDs. acdream's host normalizes every icon id through one function so both styles work everywhere an icon id is accepted: ```csharp // AcDream.Plugin.Abstractions.PluginIcons static uint Normalize(uint idOrIndex); // 0 -> 0 (no icon) // 7735 -> 0x06001E37 (bare index -> RenderSurface DID) // 0x06002D14 -> 0x06002D14 (already a DID, unchanged) ``` The host applies `Normalize` at **every** `did`-shaped sink: the descriptor's `IconSurfaceId` (drawn on the plugin shelf button), and every `` / `