# Generated code contract

What `packages/compiler` must emit for `com.uigenx.runtime` to load it. This file is the single source
of truth for both sides. Anything not written here is not guaranteed.

Verified against Unity 6000.5.9f1. Every signature below is copied from the source, not paraphrased.

## 0. At a glance: what travels how

| Artifact | Form | How the runtime gets it |
| --- | --- | --- |
| `Hud.uxml`, `Hud.uss` | files | bundle file, imported to `Assets/<UiFolder>/…` |
| `HudScreen.g.cs` | C# | compiled into the game assembly |
| `HudActions.g.cs` | C# | compiled into the game assembly |
| `UgxScreenFactory.g.cs` | C# | compiled, installs itself on load |
| screen list | `sync.bundle.manifest` over the WebSocket, **or** `Generated/ugx-manifest.json` | `UgxScreenRegistryBuilder.Rebuild` / `RebuildFromDisk` |
| binding table | C# array in `HudScreen.g.cs` | `ApplyBindings` |
| event table | C# array in `HudScreen.g.cs` | `BindActions` |
| clips and triggers | C# arrays in `HudScreen.g.cs` | `Clips` / `InstallTriggers` |
| material and filters | USS; C# table only for data driven params | `UgxMaterialBinder.Adopt` / `UgxFilterController` |

**The manifest is the only JSON the package reads.** Nothing reads `*.clips.json` or
`*.effects.json`. Tables are emitted as C# so they are typed, tree-shakeable and free of a parse
step at startup.

## 0.1 How the package reaches a project

It is copied in. `ugx export --to unity` writes `Runtime/` and `Editor/` into
`Assets/<UiFolder>/Uigenx/`, `.meta` files included, so the GUIDs of the vendored scripts are the
same in every project and a scene reference survives a re-export.

- The package is **not installed** through UPM, npm or git. A `file:` dependency is a path relative
  to `Packages/`, so a manifest committed on one machine points at nothing on the next one and Unity
  refuses to open the project at all.
- The version is recorded as `runtime` in `Generated/ugx-manifest.json`. Upgrading the CLI and
  exporting again is what upgrades the runtime, which is why the generated code and the runtime it
  loads can never be two different versions.
- The test assembly, the samples and `package.json` stay behind: inside `Assets` they are not a
  package, only files Unity would import.
- A project that also declares `com.uigenx.runtime` in `Packages/manifest.json` has two assemblies
  named `Uigenx.Runtime` and two of every GUID. The export says so; the fix is to drop the entry.
- The studio bridge under `Runtime/Bridge` stays behind `#if UGX_BRIDGE || UNITY_EDITOR`, so a
  player build never carries it. A HUD that shipped needs nothing from the studio.

## 1. Screen class

Base class `Uigenx.UgxScreen`. The generated half is `partial` so hand written code lives beside it in
`HudScreen.cs` and survives a re-export.

```csharp
public sealed partial class HudScreen : UgxScreen
{
    public const string Key = "Hud";
    public override string ScreenKey => Key;
    ...
}
```

`Key` is the screen slug exactly as the registry, the factory and the studio use it. Case sensitive,
compared with `StringComparer.Ordinal`. It is also the `name` given to the screen's container
element, so keep it stable.

### Members the generated class must declare

```csharp
protected override UgxClip[] Clips { get; }                          // return the static clip array
protected override void AttachElements(VisualElement root);
protected override void ConnectBindings(IUgxDataSource source);
protected override void ConnectActions();
protected override void ConnectEffects();

protected override void OnScreenCreate();                            // => OnCreate();
protected override void OnScreenBind(IUgxDataSource source);         // => OnBind(source);
protected override void OnScreenShow();                              // => OnShow();
protected override void OnScreenHide();                              // => OnHide();

partial void OnCreate();
partial void OnBind(IUgxDataSource source);
partial void OnShow();
partial void OnHide();
```

`partial void` cannot also be an override, which is why the base declares four `OnScreen*` virtuals
and the generated class emits four one-line forwards. If a screen has no hand written half the
`partial void` bodies are never compiled and the forwards cost nothing.

### Protected helpers available to generated code

```csharp
protected T Q<T>(string name) where T : VisualElement;               // throws UgxElementNotFoundException
protected T QOptional<T>(string name) where T : VisualElement;       // null when absent
protected void ApplyBindings(IReadOnlyList<UgxBindingEntry> table, IUgxDataSource source);
protected void BindActions(IReadOnlyList<UgxEventEntry> table);
protected void InstallTriggers(IReadOnlyList<UgxAnimationTrigger> triggers);
protected UgxDisposableSet Subscriptions { get; }                    // disposed on Detach
protected UgxBindingSet Bindings { get; }
public void Emit(string action, UgxActionPayload payload = default);
```

Emit tables as `static readonly` arrays, not instance fields: a screen instance then carries no per
instance setup cost and the arrays are shared across every open copy of the screen.

### Discovery: factory switch, not reflection

```csharp
public sealed partial class UgxScreenFactory : IUgxScreenFactory
{
    public bool Contains(string key) => key == HudScreen.Key;

    public UgxScreen Create(string key)
    {
        switch (key)
        {
            case HudScreen.Key: return new HudScreen();
            default: return null;
        }
    }

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
#if UNITY_EDITOR
    [UnityEditor.InitializeOnLoadMethod]
#endif
    static void Install() => UgxScreens.Install(new UgxScreenFactory());
}
```

Both attributes are required: `RuntimeInitializeOnLoadMethod` covers the player and play mode,
`InitializeOnLoadMethod` covers edit mode so the studio can render a preview without entering play
mode. `UgxScreens.Install` is idempotent per factory type, so a repeated callback cannot stack
duplicates. `Create` returning null means "not mine"; several assemblies may each install a factory
and the most recently installed one that owns a key wins.

## 2. Registry and manifest

### Two ways in, one shape

```csharp
UgxScreenRegistryBuilder.Rebuild(UgxBundleManifest manifest, string assetsUiFolder)  // live bundle
UgxScreenRegistryBuilder.RebuildFromDisk(string assetsUiFolder = null)               // offline export
```

Both end in the same code, so a registry built offline is identical to one built from a sync. There
is a test that pins that equivalence.

The parsed shape is exactly the zod `SyncBundleSchema.manifest` in `packages/bridge-protocol`:

```
manifest.screens[] : { key, uxml, className, layer }
manifest.actions[] : { name, payload }          // accepted, currently unused by the runtime
manifest.panels[]  : { name, path, sortOrder, scaleMode, scale, referenceDpi, fallbackDpi,
                       referenceResolution, screenMatchMode, match,
                       referenceSpritePixelsPerUnit, theme }
```

Field by field, for `screens[]`:

| Field | Type | Missing or unparsable | Notes |
| --- | --- | --- | --- |
| `key` | string | entry **skipped silently** | must be non-empty |
| `uxml` | string | `VisualTreeAsset` is null, **warning**, entry still added | path **relative to the UI folder**, e.g. `Generated/Screens/Hud/Hud.uxml` |
| `className` | string | stored as null | **diagnostics only**, never used to reflect |
| `layer` | string | falls back to `Hud`, no warning | one of `scene hud overlay modal system` |

Two behaviours to design around:

- **Stylesheets are not in the manifest.** The builder loads exactly one: the `.uss` sitting next to
  the UXML with the same base name (`Path.ChangeExtension(uxmlPath, ".uss")`). Emit `Hud.uxml` and
  `Hud.uss` in the same folder. Any other stylesheet must be referenced from inside the UXML with a
  `<Style src="…">` element.
- **`PanelSettings` is never set per screen** by the builder; the entry's panel is always null and
  the screen uses its layer's panel. Per screen panels exist in `UgxScreenEntry` for hand authored
  registries only.

### Panels

`UgxPanelBuilder` runs on the same pass as the registry and turns every `manifest.panels[]` entry
into a `PanelSettings` asset at `Assets/<UiFolder>/<path>`. The compiler cannot write one itself:
the asset holds an asset reference to a theme, so only the editor can produce one that survives a
re-import.

- The studio owns every field the plan carries and they are rewritten on each import. Anything the
  plan does not describe, the target texture or the clear colour, is left alone.
- `scaleMode` is `constant-pixel-size`, `constant-physical-size` or `scale-with-screen-size`;
  `screenMatchMode` is `width-or-height`, `shrink` or `expand`. An unknown spelling falls back to
  the scale-with-screen-size defaults rather than to Unity's constant pixel size, because a HUD
  drawn at constant pixel size looks right only at the resolution it was authored on.
- `theme` names a `.tss` inside the UI folder. When it is empty and the panel has none, the builder
  creates `Assets/UI Toolkit/UnityThemes/UnityDefaultTheme.tss` holding
  `@import url("unity-theme://default");`, the file Unity itself writes when a UIDocument is added
  by hand. A theme the project already chose is never replaced.
- The panel with the lowest `sortOrder` becomes `UgxSettings.defaultPanelSettings`, but only when
  the project has not chosen one.

`Rebuild` writes `Assets/<UiFolder>/Generated/Resources/UgxScreenRegistry.asset`, creates
`UgxSettings.asset` beside it if the project has none, points settings at the registry, and sets
`UgxScreenRegistry.Active`. A rebuild **replaces every entry**, so a screen dropped from the
manifest disappears from the registry.

### The offline manifest file

`ugx export --to unity` must keep writing:

```
Assets/<UiFolder>/Generated/ugx-manifest.json
```

Exact shape, camelCase, nothing else required:

```json
{
  "schemaVersion": 1,
  "screens": [
    { "key": "Hud", "uxml": "Generated/Screens/Hud/Hud.uxml",
      "className": "Game.UI.Generated.HudScreen", "layer": "hud" }
  ],
  "actions": [ { "name": "hud.pause", "payload": {} } ],
  "panels":  [ { "name": "Main", "path": "Panels/Main.asset", "sortOrder": 100 } ],
  "fonts":   [ { "source": "Fonts/title.ttf", "path": "Fonts/title SDF.asset",
                 "guid": "1234567890abcdef1234567890abcdef", "renderMode": "SDFAA",
                 "samplingPointSize": 90, "atlasSize": 1024, "padding": 9 } ]
}
```

- **Keep `schemaVersion`.** It is an int, currently `1`. A file with a higher version is refused
  with a message telling the user to update the package, rather than being half read. A missing
  `schemaVersion` is treated as `1`, so an older emitter keeps working.
- `screens`, `actions` and `panels` are the same objects as on the wire; `actions` and `panels` may
  be omitted. Drop `version`, `projectId`, `exportRoot` and `templates` as you planned; nothing
  reads them, and unknown members are ignored if you leave them in.
- `uxml` is relative to the UI folder, the same as on the wire.

The registry rebuilds by itself when this file is imported or moved: `UgxManifestPostprocessor`
watches exactly that path and defers the rebuild out of the import callback. In batch mode it
rebuilds inside the callback instead, because `delayCall` is never pumped by a `-batchmode -quit`
run and the offline path would otherwise register nothing. So dropping an offline
export into `Assets/` is enough, with no menu click and no daemon.

For a headless export, call the entry point directly:

```
Unity -batchmode -quit -projectPath <project> \
      -executeMethod Uigenx.Editor.UgxScreenRegistryBuilder.RebuildFromDiskCommandLine
```

`Tools > UI Creator > Rebuild screen registry` reads the file too, and only falls back to writing an
empty registry when there is none.

Malformed JSON, a missing file and a future schema version are all reported as warnings and build
nothing; they never throw into the asset pipeline.

### Font assets

An SDF atlas is rendered by Unity's own text engine, so the compiler ships the font file and the
recipe rather than the artefact. Each `fonts` entry names a font file already in the export and the
FontAsset to bake from it. `UgxFontAssetBuilder` runs on the same postprocessor as the registry and
on every `sync.bundle`, so an offline export bakes without a daemon.

- The asset is created with `FontAsset.CreateFontAsset` in `AtlasPopulationMode.Dynamic`, the only
  population mode the Advanced Text Generator supports besides Dynamic OS. The atlas texture and
  the material are added as sub assets, or they are dropped on the next domain reload.
- `guid` is honoured only for an asset this package creates. On a rebake the GUID already on disk
  wins, so a scene reference survives a settings change.
- Nothing is rebaked when the render mode, padding, atlas size and sampling point size all match
  what the asset already has.
- Turning the bake off leaves the asset in place. It was created once and may be referenced by
  something the export cannot see, so removing it is the user's call.
- A missing or unimported font file is a warning, never a throw.

`Tools > UI Creator > Rebuild font assets` runs the same pass by hand.

## 3. Bindings

### There is no `Bind(...)` method

The runtime takes a table, not per binding calls:

```csharp
public readonly struct UgxBindingEntry
{
    public UgxBindingEntry(string elementName, string property, string path,
        BindingMode mode = BindingMode.ToTarget, UgxBindingEngine engine = UgxBindingEngine.Auto,
        UgxFormatKind format = UgxFormatKind.None, int decimals = 0, string formatText = null);
}
```

There is no `UgxFormat` factory class. Formatting is the `UgxFormatKind` enum plus `decimals` and
`formatText`:

| `UgxFormatKind` | Uses | Result |
| --- | --- | --- |
| `None` | | value as is, invariant `ToString` for text targets |
| `Number` | `decimals` | `"F{decimals}"`, invariant |
| `Percent` | `decimals` | value multiplied by 100, `"F{decimals}"`, then `%` |
| `Template` | `formatText` | `string.Format(InvariantCulture, formatText, value)`, `{0}` is the value |
| `Custom` | `formatText` | registered `ConverterGroup` id; falls back to plain text if unknown |

`BindingMode` is Unity's own: `ToTarget`, `ToTargetOnce`, `ToSource`, `TwoWay`.

`UgxBindingEngine` is `Auto`, `Unity`, `Runtime`.

Generated form:

```csharp
static readonly UgxBindingEntry[] BindingTable =
{
    new UgxBindingEntry("health-value", "text", "hp", BindingMode.ToTarget,
        UgxBindingEngine.Runtime, UgxFormatKind.Template, 0, "{0} / 100"),
    new UgxBindingEntry("health-fill", "width", "hpNormalised", BindingMode.ToTarget,
        UgxBindingEngine.Runtime, UgxFormatKind.Percent),
};

protected override void ConnectBindings(IUgxDataSource source) => ApplyBindings(BindingTable, source);
```

An empty `elementName` means the screen root.

### `ConnectBindings` must always call `ApplyBindings`

Even when the table is empty. `ApplyBindings` is what sets `root.dataSource`, and native UXML
`<Bindings>` resolve against that inherited value. A screen that skips the call because it has no
C# bindings will silently break every native binding in its UXML.

### Native UXML bindings and the table must not overlap

`element.SetBinding(id, binding)` replaces by `BindingId`, so emitting both a `<ui:DataBinding
property="text">` and a table entry for `text` on the same element means the table silently wins.
Pick one per element and property.

### When native UXML binding actually works

This is the runtime's own decision function, and `Auto` follows it exactly:

```csharp
public static bool UseUnityBinding(in UgxBindingEntry entry, IUgxDataSource source)
{
    if (entry.Engine == UgxBindingEngine.Runtime) return false;
    if (entry.Engine == UgxBindingEngine.Unity) return true;
    if (entry.Format != UgxFormatKind.None) return false;
    if (IsStyleProperty(entry.Property)) return false;
    return source is UgxDataSource || source is UgxReflectiveDataSource;
}
```

So a native UXML `<ui:DataBinding>` is correct only when **all** of these hold: no formatter, the
target is a normal bindable property rather than a style property, and the data source resolves
paths through a `Unity.Properties` property bag. A `UgxDictionaryDataSource` resolves paths as
literal dictionary keys and has no property bag, so a dotted path will not resolve natively. The
studio preview and the live data stream both use a dictionary source, so **preview only agrees with
play mode if the binding is emitted as a table entry**. Prefer the table unless you know the
project's source derives from `UgxDataSource`.

### Style targets

`IsStyleProperty` is true for a `style.` prefix or any name in the style setter table. Both spellings
work, with or without the prefix:

```
opacity  translate  scale  rotate  width  height  left  top
background-color (backgroundColor)
color
-unity-background-image-tint-color (unityBackgroundImageTintColor)
```

Anything else is not animatable or bindable as a style and is rejected with a warning.

### The percent rule

A style value goes through the setter's own USS parser, so **the unit comes from the text**:

- source value is a **string** such as `"25%"` or `"12px"` → use `UgxFormatKind.None`, the unit is
  taken from the string.
- source value is a **number already in pixels** → `UgxFormatKind.None`, applied as pixels.
- source value is a **normalised 0..1 number** bound to `width`, `height`, `left` or `top` →
  **`UgxFormatKind.Percent` is required**. Without it the value is formatted as a bare number and
  parsed as pixels, so `0.75` becomes `0.75px` instead of `75%`. This is a silent visual failure,
  not an error.
- `opacity`, `scale` and other unitless targets must **not** use `Percent`: `"75%"` fails to parse
  as a float and the binding reports a failure.

### Two way

`TwoWay` and `ToSource` also register write back on `ChangeEvent<bool|string|int|float>` for the
bound element, filtered to that element so a child control's edit is not written to a parent's path.
A control whose value type is none of those four needs a hand written subscription.

## 4. Actions

### Event table

```csharp
public readonly struct UgxEventEntry
{
    public UgxEventEntry(string elementName, UgxUiEvent uiEvent, string action, string nodeId = null,
        Func<UgxActionPayload> payload = null);
}
```

The enum is `UgxUiEvent`, not `UgxEventKind`. Members and their studio wire names:

| Member | Wire name | Unity event |
| --- | --- | --- |
| `Click` | `click` | `ClickEvent` |
| `PointerDown` | `pointer-down` | `PointerDownEvent` |
| `PointerUp` | `pointer-up` | `PointerUpEvent` |
| `PointerEnter` | `pointer-enter` | `PointerEnterEvent` |
| `PointerLeave` | `pointer-leave` | `PointerLeaveEvent` |
| `Change` | `change` | `ChangeEvent<bool\|string\|int\|float>` |
| `Submit` | `submit` | `NavigationSubmitEvent` |
| `Focus` | `focus` | `FocusEvent` |
| `Blur` | `blur` | `BlurEvent` |
| `NavigationSubmit` | `navigation-submit` | `NavigationSubmitEvent` |
| `NavigationCancel` | `navigation-cancel` | `NavigationCancelEvent` |
| `LongPress` | `long-press` | pointer down held 500 ms |

`Submit` and `NavigationSubmit` map to the same Unity event; both names exist so the studio model
round trips. `UgxUiEvents.TryParse` and `ToWireName` convert between the two spellings.

Generated form:

```csharp
static readonly UgxEventEntry[] EventTable =
{
    new UgxEventEntry("pause", UgxUiEvent.Click, HudActions.Pause, "n_pause"),
    new UgxEventEntry("item-0", UgxUiEvent.Click, HudActions.UseItem, "n_item0", PayloadForSlot0),
};

protected override void ConnectActions() => BindActions(EventTable);

static UgxActionPayload PayloadForSlot0() => UgxActionPayload.Of(HudActions.SlotKey, 0);
```

`nodeId` is the studio node id and is carried through to `action.fired` so the studio can highlight
the element that raised it. A row whose element is missing is skipped with a warning; one bad name
never breaks a screen.

### Payload

`UgxActionPayload` is a struct over an optional dictionary. `default` allocates nothing, which is
why the factory is `null` rather than a factory returning an empty payload when there is no payload.

```csharp
UgxActionPayload.Of(string key, object value)
UgxActionPayload.Of(string k0, object v0, string k1, object v1)
payload.With(key, value)      // allocating copy, off hot paths
```

A `Change` event automatically adds the new value under the key `"value"`
(`UgxActionBinder.ChangeValueKey`).

### Action helper class

```csharp
public static class HudActions
{
    public const string Pause = "hud.pause";
    public const string UseItem = "hud.use-item";
    public const string SlotKey = "slot";

    public static IDisposable OnPause(Action handler)
        => UgxActionBus.Subscribe(Pause, _ => handler());

    public static IDisposable OnUseItem(Action<int> handler)
        => UgxActionBus.Subscribe(UseItem, action => handler(action.Payload.GetInt(SlotKey)));

    public static void EmitUseItem(int slot)
        => UgxActionBus.Emit(UseItem, UgxActionPayload.Of(SlotKey, slot));
}
```

One `On<Name>` per action, typed to the declared payload; one `<Key>Key` const per payload field; an
`Emit<Name>` for actions game code should be able to raise. Payload type to getter:
`string`→`GetString`, `int`→`GetInt`, `float`→`GetFloat`, `bool`→`GetBool`. Everything returns an
`IDisposable`; game code puts it in `Subscriptions` so it is released on `Detach`.

Names the runtime reserves, do not emit them: `system.modal-opened`, `system.modal-closed`,
`system.screen-shown`, `system.screen-hidden`.

## 5. Animation

Clips are **C# in `HudScreen.g.cs`**, not `*.clips.json`. Nothing loads that file.

```csharp
new UgxClip(string name, float durationMs, UgxLoopMode loop, params UgxTrack[] tracks)
new UgxTrack(string targetName, string property, params UgxKeyframe[] keyframes)
new UgxTrack(string targetName, string partClass, string property, params UgxKeyframe[] keyframes)
new UgxKeyframe(float timeMs, string value, UgxEasingCurve easing = default)
```

- **Time is absolute milliseconds, not normalised.** A keyframe at the end of a 220 ms clip is
  `new UgxKeyframe(220f, "1")`, not `1.0`. Keyframes must be sorted ascending.
- `targetName` empty or null means the screen root. Otherwise it is an element `name` looked up
  under the screen root.
- `partClass` drives a sub element the control builds for itself instead of the control: the fill of
  a ProgressBar is `AbstractProgressBar.progressUssClassName`, `"unity-progress-bar__progress"`.
  Those sub elements carry a USS class and no name, so the animator resolves one with
  `Q(owner, null, partClass)` (`UIElements.UQueryExtensions.Q(VisualElement, string, string)`), the
  first descendant of the target carrying that class. Emit the **USS class**, not the studio's part
  name. A class the target does not build logs a warning and the track is skipped.
- `property` must be one of the style setter names in section 3.
- `value` is canonical USS value text, parsed **once** when the clip is prepared. Formats accepted:
  number (`0.5`), length (`16px`, `50%`, bare number means px), pair (`0 16px`, one value applies to
  both axes), angle (`45deg`, `0.5turn`, `1rad`, `100grad`), colour (`#rgb`, `#rrggbb`, `#rrggbbaa`,
  named, `rgb()`, `rgba()`).
- The easing on a keyframe shapes the segment **leaving** it, matching USS.
- `UgxLoopMode` is `None`, `Loop`, `PingPong`.

### Easing

```csharp
UgxEasingCurve.Of(UgxEasingKind.EaseOutCubic)
UgxEasingCurve.CubicBezier(0.25f, 0.1f, 0.25f, 1f)
```

The 23 keywords, matching `EASING_KEYWORDS` in the studio schema and Unity's `EasingMode`:

```
ease  ease-in  ease-out  ease-in-out  linear
ease-in-sine  ease-out-sine  ease-in-out-sine
ease-in-cubic ease-out-cubic ease-in-out-cubic
ease-in-circ  ease-out-circ  ease-in-out-circ
ease-in-elastic ease-out-elastic ease-in-out-elastic
ease-in-back  ease-out-back  ease-in-out-back
ease-in-bounce ease-out-bounce ease-in-out-bounce
```

`UgxEasing.TryParse(wireName, out kind)` converts from the studio spelling. `UgxEasingKind.Bezier`
is the 24th member and is only produced by `CubicBezier`.

### Triggers

```csharp
new UgxAnimationTrigger(UgxTriggerKind kind, string clip, string target = null, string key = null,
    UgxPlayMode mode = UgxPlayMode.Restart, UgxTriggerCondition condition = default)
```

`UgxTriggerKind` is `Show`, `Hide`, `Hover`, `HoverExit`, `Action`, `DataChange`.
`UgxPlayMode` is `Restart`, `IgnoreIfPlaying`, `Queue`.

Lowering the studio `AnimationTrigger` model:

| Studio trigger | Emit |
| --- | --- |
| `{kind:'lifecycle', phase:'show'}` | `UgxTriggerKind.Show` |
| `{kind:'lifecycle', phase:'hide'}` | `UgxTriggerKind.Hide` |
| `{kind:'state', pseudo:'hover', target}` | `Hover`. `reverseOnExit` is **not** emitted: the animator plays forward only, so reversing needs a mirrored clip the compiler does not generate yet, and a diagnostic says so |

A track target is an element **name**, resolved with `Q<VisualElement>(name)`, so a native USS
transition is emitted as `#name`, never `.name`. A `{self: true}` track is the screen root, matching
`UgxAnimator`'s `track.TargetsSelf ? _root : _root.Q<VisualElement>(track.TargetName)`. A target no
element carries emits `UC-ANIM-TARGET` instead of a selector that would load and never fire.

A `{name, part}` track drives a sub element of a control and lowers to a descendant selector,
`#name .unity-progress-bar__progress`. The pseudo class stays on the **owner**, as in the manual's
own `.slide-toggle:focus .slide-toggle__input-knob` (`Manual/UIE-slide-toggle.html`), so the state
rule is `#name:hover .unity-progress-bar__progress`. Each part gets its own rule, because a part is
a different element in the tree. `target` on the trigger names the element whose pseudo state fires
the clip and never a part: the control is what the pointer is over.

| `{kind:'state', pseudo: anything else}` | **not supported**, emit a compiler diagnostic |
| `{kind:'data', path, when:'change'}` | `DataChange` with `key = path`, no condition |
| `{kind:'data', path, when:{op,value,play}}` | `DataChange` with `key = path` plus a `UgxTriggerCondition` |
| `{kind:'event', event:'pointer-enter'}` | `Hover` |
| `{kind:'event', event:'pointer-leave'}` | `HoverExit` |
| `{kind:'event', event: other}` | no direct mapping; emit an `Action` trigger keyed on the action that element already raises, or a diagnostic |

`target` is only read for `Hover` and `HoverExit`. `key` is the action name for `Action` and the data
path for `DataChange`, and the path must be **exactly** the string the data source raises (section 7).

A `DataChange` trigger needs a source implementing `INotifyBindablePropertyChanged`; the runtime logs
"it will never fire" otherwise.

### Conditional data triggers

```csharp
public enum UgxCompare { Always = 0, Lt, Lte, Gt, Gte, Eq, Neq }
public enum UgxConditionPlay { Loop = 0, Once = 1 }

UgxTriggerCondition.Number(UgxCompare op, double threshold, UgxConditionPlay play = Loop)
UgxTriggerCondition.Text(UgxCompare op, string literal, UgxConditionPlay play = Loop)
```

`default(UgxTriggerCondition)` is `UgxCompare.Always`, which is the absence of a condition: the clip
plays on every change to the path, the pre-condition behaviour.

Generated form for `player.hpNormalised lt 0.25`:

```csharp
new UgxAnimationTrigger(UgxTriggerKind.DataChange, "hp-pulse", null, "player.hpNormalised",
    UgxPlayMode.IgnoreIfPlaying, UgxTriggerCondition.Number(UgxCompare.Lt, 0.25, UgxConditionPlay.Loop)),
```

- **Numeric thresholds must be emitted with a decimal point** (`10.0`, not `10`), or the C# overload
  resolves to `Text`.
- The runtime evaluates on **edges only**: it re-reads `IUgxDataSource.TryGet(key)` on every change to
  the path, and acts when the answer flips. A value that keeps satisfying the condition never
  restarts the clip, so a looping pulse runs continuously instead of stuttering.
- `UgxConditionPlay.Loop` calls `UgxAnimator.Rewind(clip)` on the falling edge: the clip stops and
  every track returns to its first keyframe. A plain `Stop` would freeze the element mid animation.
- `UgxConditionPlay.Once` fires on the rising edge and is never stopped, so the clip finishes.
- The condition is evaluated once at `InstallTriggers` time, so a screen that binds with the
  condition already true starts the clip immediately.
- A path that does not resolve satisfies nothing, `Neq` included.
- An empty `propertyName` (a source's "everything changed", such as `UgxDictionaryDataSource.Clear`)
  re-evaluates every data trigger.
- Ordering comparisons need a numeric value; `Eq` and `Neq` accept a text threshold and then compare
  `Convert.ToString(value, InvariantCulture)`, with booleans spelled `true` and `false` to match the
  studio. `packages/schema`'s `conditionHolds` is the same truth table and both sides must stay in
  step, or a screen behaves one way in Preview and another in the game.

Wire it up with:

```csharp
protected override UgxClip[] Clips => ClipTable;
protected override void ConnectEffects() => InstallTriggers(TriggerTable);
```

Do not animate a property that a binding also drives on the same element. There is no arbitration,
so the last writer in a frame wins and the result flickers.

## 6. Effects

### Material: assign in USS, drive parameters in C#

`-unity-material` is confirmed in the 6000.5 manual (`Manual/UIE-USS-SupportedProperties.html`):

```
-unity-material: <resource> | <url> | none
```

> The `-unity-material` property assigns a custom material to the element. **Note**: This property
> is automatically inherited by descendant elements, meaning child elements use the same material
> but they can override it.

`Manual/UIE-USS-Properties-Reference.html` also lists it as inherited and **fully animatable**.

**Emit the material assignment in USS, not in C#.** Generated C# cannot hold an asset reference
without a `Resources.Load`, whereas USS resolves the asset at import time. So:

```css
.glow-panel {
    -unity-material: url("/Assets/UI/Materials/glow.mat");
}
```

**Use the documented absolute form `url("/Assets/…")`, not `url("project://database/…")`.** The
manual lists exactly these spellings for `<url>` (`Manual/UIE-USS-PropertyTypes.html`): a relative
path, `url("/Assets/…")`, `url("project:/Assets/…")` and `url("project:///Assets/…")`. The
`project://database/…` host form your emitter uses today is what UI Builder serialises, and it works
because the same URL resolver handles it, but it is undocumented and normally carries a
`?fileID=…&guid=…&type=…` query that the compiler would then have to keep in sync with the importer.
The plain project-absolute path has no GUID to drift, diffs cleanly, and the compiler owns the whole
`Generated/` tree so the path is stable by construction.

Two caveats I owe you honestly:

- **There is no real-world example of `-unity-material` anywhere on this machine.** Not in the 54
  corpus stylesheets, not in 453 stylesheets in a production project, not in the 303 the editor
  ships. The property is new. The grammar above is quoted from the manual; that `url()` resolves for
  *this particular property* is inference from the generic `<url>` rule, not something I observed.
  Worth one manual smoke test on your side before you rely on it.
- `none` and `initial` are accepted. `unset` is not documented and I would not emit it.

### Material parameters

| Kind of value | Where it belongs |
| --- | --- |
| constant, same for every element | the `.mat` asset |
| varies per element, or comes from data | generated C#, through `UgxMaterialBinder` |

Constants should go in the material asset, not in generated C#: they cost nothing at runtime and are
visible in the inspector. Emit the `.mat` as a bundle file of kind `mat`; the importer writes it
verbatim with a stable GUID. Note that `sync.bundle.materials[]` is currently **parsed and ignored**,
so shipping the material as a file is the only route that works today.

For data driven parameters, emit a table and adopt what USS resolved:

```csharp
static readonly UgxMaterialEntry[] MaterialTable =
{
    new UgxMaterialEntry("glow-panel",
        new UgxMaterialParam("_Tint", UgxMaterialParamKind.Color, "team.color"),
        new UgxMaterialParam("_Power", UgxMaterialParamKind.Float, "player.glow")),
};

protected override void ConnectEffects()
{
    ApplyMaterials(MaterialTable);
    InstallTriggers(TriggerTable);
}
```

There is deliberately no material reference and no `InstancePerElement` in the entry.
`UgxMaterialBinder.Adopt(element)` reads `resolvedStyle.unityMaterial` on the first geometry pass and
drives its parameters from there; parameter bindings made before that are queued and applied when it
resolves. Per element values do not need instancing because the values live in the element's
`MaterialDefinition` rather than on the shared asset. A row is only needed when at least one
parameter is data driven. `UgxMaterialParamKind` is `Float`, `Color`, `Vector`.

URP only. On any other pipeline the binder warns once and the element renders unstyled.

### Filters

Confirmed grammar (`Manual/ui-systems/built-in-filters.html`):

```
filter: blur(<length>) | grayscale(<number>) | invert(<number>) | opacity(<number>)
      | sepia(<number>) | tint(<color>) | hue-rotate(<angle>) | contrast(<number>)
```

The manual's own literals: `blur(20px)`, `grayscale(100%)`, `invert(100%)`, `opacity(50%)`,
`sepia(100%)`, `tint(#00ff99)`, `hue-rotate(90deg)`, `contrast(200%)`. Chained with **spaces, not
commas**: `filter: blur(5px) invert(100%);`. `drop-shadow` does not exist in USS.

Two behaviours that affect codegen:

- **Filters do not cascade additively.** If two classes on the same element each set `filter`, the
  last one wins outright rather than merging. Emit one `filter` declaration per element state.
- **Transitions only interpolate when the function list matches in type and order.** A partial match
  pads with defaults; a reordering snaps. If a screen transitions filters, emit the same functions in
  the same order in both states.

**Your custom filter form is correct.** From `Manual/ui-systems/custom-filters.html`:

```css
.custom-effect      { filter: filter("/Assets/Filters/CustomFilter.asset" 10px); }
.custom-effect-blur { filter: filter("/Assets/Filters/CustomFilter.asset" 10px) blur(25px); }
```

The function is literally named `filter`, it takes a quoted asset path followed by space separated
parameters with no comma, and custom and built-in functions mix in one declaration. Relative paths
work too: the manual's swirl example shows UI Builder itself writing
`filter: filter("SwirlFilter/SwirlFilterFunction.asset" 58.9 2.3);`.

Custom filter parameters are `float` or `color` only; numbers, pixels, percentages, durations and
angles are all converted to float. Note that `blur`'s float is a **sigma**, not a radius, even though
the USS literal is written in px.

`UgxFilterController` covers the animated and data driven cases in C#, reusing one
`List<FilterFunction>` so pushing it every tick allocates nothing. `TryAdd(name, args)` accepts the
eight built-in names above. Static filters need no C# at all; leave them in USS.

## 7. Data source paths and change notification

### Path rules

A path is a member path **within the data source object**. It is not prefixed with the source name
or the type name.

| Source shape | Correct path | Wrong |
| --- | --- | --- |
| `class Player { int hp; }` used as the source | `hp` | `player.hp`, `Player.hp` |
| `class Root { Player player; }` used as the source | `player.hp` | `hp` |
| `UgxDictionaryDataSource` | whatever key was set, e.g. `player.hp` | anything else |
| array element | `inventory.items[0].count` | |

So nesting is fine and mirrors the object graph; what must not appear is a synthetic prefix naming
the source. `UgxSchemaWalker.Collect(type)` produces exactly the set of legal paths for a POCO or
`UgxDataSource`, and `dataSchema.result` reports that same set. **Bind only to paths the schema
reported.** A path that does not resolve fails on every update, silently as far as the UI is
concerned.

`UgxDictionaryDataSource` is the exception: it is keyed by the full path string, so any key works,
and `Merge` flattens a nested tree into path keys and additionally writes `<path>.length` for arrays.

### Change notification

**A data source that cannot announce a change renders once and then freezes.** Since the last
revision the connector logs a warning naming the screen and the count when it sees one, but the
compiler should still steer authors correctly.

Three ways to get it right, in order of preference:

1. derive from `Uigenx.Binding.UgxDataSource`, mark members `[CreateProperty]`, call `Notify()` from
   every setter;
2. use `UgxDictionaryDataSource`, which notifies on every `Set`;
3. implement `INotifyBindablePropertyChanged` and `IDataSourceViewHashProvider` by hand, or forward
   them to an inner source. A `MonoBehaviour` source must do this, because `UgxDocument`'s
   serialized field takes a `UnityEngine.Object`. `Samples~/MinimalHud/HudDataSource.cs` shows the
   forwarding pattern.

Sharp edge worth putting in the generated doc comment: **`Notify()` uses `[CallerMemberName]`, so a
nested source raises `hp`, not `player.hp`.** A source with nested objects must call
`Notify("player.hp")` explicitly, with the full path the binding uses, or the binding will not
refresh. A derived read only property must also be notified from every setter it depends on.

## 8. Files, folders and naming

```
Assets/<UiFolder>/Generated/
  Screens/Hud/Hud.uxml
  Screens/Hud/Hud.uss              <- must share the base name and folder with the uxml
  Screens/Hud/HudScreen.g.cs
  Screens/Hud/HudActions.g.cs
  UgxScreenFactory.g.cs
  Resources/UgxScreenRegistry.asset   <- written by the editor, not by the compiler
  Resources/UgxSettings.asset         <- created by the editor if absent
```

`<UiFolder>` comes from `ProjectSettings/UgxSettings.json`, default `UI`. Bundle file paths are
relative to `Assets/<UiFolder>`. A path that escapes that folder is refused with a warning, and
`deletions` only apply to paths the importer previously wrote (tracked in `Library/UgxImported.json`).

Layer wire names: `scene`, `hud`, `overlay`, `modal`, `system`, with panel sort orders
0, 100, 200, 300, 400.

## 9. Obligations on the daemon and the wire

1. **Push `sync.bundle` when `hello` arrives.** Protocol v1 has no `sync.request`; the editor's
   "Sync now" reconnects to trigger one.
2. **Send `ping` every 15 s.** Unity cannot originate a ping in v1. It answers with `pong` and closes
   plus reconnects after 30 s of inbound silence.
3. **Dictionary keys are not camelCased on the wire.** `data.snapshot.values`, `action.*.payload` and
   `preview.request.data` keys travel verbatim so data paths survive.
4. **Mirror pseudo state rules onto `ugx-state-<name>` classes** in the exported USS. A runtime panel
   cannot be forced into `:hover`, so `preview.request.states` is applied as those classes.
5. `preview.request.data` is a nested tree; it is flattened with the `.length` convention above.
6. GUIDs in `sync.bundle.files[].guid` may be upper or lower case; they are normalised to lowercase.
   An existing `.meta` always wins, so a re-export never renumbers an asset.
7. `.meta` ScriptedImporter ids, confirmed against real files: `.uxml` 13804, `.uss` 12385,
   `.tss` 12388, all with guid `0000000000000000e000000000000000`. `.uss` and `.tss` carry a
   trailing `disableValidation: 0`.

## 10. Open decisions

- `ActionDecl.preview` in the studio document is editor only and is never lowered. The runtime has
  no action to screen table, so a flow the studio can act out still needs game code
  (`UgxScreenManager.Push`, `Pop`, `Close`). The compiler reports every declaration as
  `UC-PREVIEW-ONLY` so an author cannot ship a flow that only ever worked in the studio. If a
  future runtime gains a declarative screen router, this is the first thing that should feed it.
- `manifest.actions` and `manifest.panels` are parsed and then ignored. Panels in particular could
  drive per layer `PanelSettings`, which today always come from `UgxSettings.DefaultPanelSettings`.
- `sync.bundle.materials[]` is parsed and ignored; see section 6 for what that means for material
  constants today.
- `*.clips.json` and `*.effects.json` have no reader. If the studio needs to round trip them they
  are fine as studio side artifacts, but they must not be the only place a clip exists.
