Everything lives in `Udonite.Binding`.

Every binding shares the same shape:

| Member | What it does |
|---|---|
| `Watch(control)` | starts watching; raises the events and nothing else |
| `Bind(control, observable)` | the same, and keeps an observable in step as well |
| `Poll()` | reads the control; call it from `Update` |
| `Unbind()` | stops watching and lets go of the observable |
| `Value` | what the control holds now, whether or not an observable is bound |

And the same rules:

- **The control is written at bind time**, so it starts showing the value rather than whatever the designer left in it.
- **Binding again replaces the previous binding** rather than adding to it, so a `Start` that runs twice leaves one.
- **A null control or observable is ignored** rather than halting the behaviour.
- **Handlers already added stay** when you `Watch` again or `Unbind` — they belong to whoever added them, not to the control. Clear the event field to drop them.
- **Only the player's changes raise an event.** Moving a control by assigning its observable does not.

## Watched controls

These need `Poll()` in your `Update`.

### SliderBinding

| Member | |
|---|---|
| `Action<float> OnChanged` | raised when the player has moved the handle |
| `Bind(Slider, Observable<float>)` | both directions |
| `float Value` | where the handle is |

### ToggleBinding

| Member | |
|---|---|
| `Action<bool> OnChanged` | raised when the player has clicked the box |
| `Bind(Toggle, Observable<bool>)` | both directions |
| `bool Value` | whether it is ticked |

### InputBinding

| Member | |
|---|---|
| `Action<string> OnChanged` | every keystroke |
| `Action<string> OnEndEdit` | the player left the field having changed it |
| `Action OnFocused` | the player started editing |
| `Bind(TMP_InputField, Observable<string>)` | both directions |
| `string Value` | what is in the field |

`OnChanged` reports **every keystroke**, which is what a field bound to a live label should do. Read `Value` once on a button press instead if that is not what you want.

Writing the observable while the player is typing moves their caret to the end. The binding does not do this to itself — a poll pushes their text in and the write back is the same string — but code that assigns during editing will.

## Driven controls

No events, no `Poll()`. An observable is how they are fed.

### TextBinding&lt;T&gt;

| Member | |
|---|---|
| `Bind(TextMeshProUGUI, Observable<T>)` | shows the value |
| `Bind(TextMeshProUGUI, Observable<T>, prefix, suffix)` | with text either side |

`Bind(label, score, "Score: ", "")` and `Bind(label, health, "", " HP")`. A null value shows as an empty string rather than faulting.

### ActiveBinding

| Member | |
|---|---|
| `Bind(GameObject, Observable<bool>)` | shown while true |
| `BindInverted(GameObject, Observable<bool>)` | shown while false |

Two bindings on one observable make a matched pair of signs.

**A hidden object's behaviours stop running.** Hiding the object a binding lives on stops its `Update`, and a `Poll()` on that object stops with it. Keep polling bindings on something that stays active.

### FillBinding

| Member | |
|---|---|
| `Bind(Image, Observable<float>)` | a value already between 0 and 1 |
| `Bind(Image, Observable<float>, min, max)` | a value over a range you name |

The range is part of the binding because `fillAmount` is 0 to 1 and almost nothing a world counts is. Values outside it are clamped, and an empty or backwards range leaves the image empty rather than dividing by zero.

The Image needs its type set to **Filled** for `fillAmount` to do anything.

## What this does not do, and why

**Buttons.** A click is an event rather than a value. `Slider.value`, `Toggle.isOn` and `TMP_InputField.text` can be read at any moment; "was clicked since last frame" cannot, so there is nothing to poll. A button still has to be wired in the inspector to a public method.

**URL fields.** A `VRCUrlInputField` accepts selection and never focuses — measured by calling `ActivateInputField()` on one directly, which leaves `isFocused` false. VRChat routes text into it through its own keyboard, and it cannot be exercised in the Editor at all. Use its `OnEndEdit` in the inspector, which is the route VRChat documents.

**Lists of rows.** Driving a set of rows from an `ObservableList<T>` means creating and destroying objects, which in a world means `VRCObjectPool` and decisions about ownership. That is a different package.

## If a handler throws

An exception in Udon halts the behaviour for the rest of the session, and there is no catching one handler's and carrying on. A handler that throws takes down the rest of the notification and everything that behaviour would have done afterwards, so keep handlers to reading the value and updating something.
