# ET UI service v1 (SDK 0.4 / M2)

`<epictuner/ui.h>` is the optional, Qt-free C service. Declare `ui` and
`et.ui.v1` in the development manifest, query `ET_SERVICE_UI` with
`ET_SERVICE_VERSION_1`, and install one dispatcher listener. ABI 1.1 and its
1.0 prefixes are unchanged. `<epictuner/sdk/ui.hpp>` adds owning `PanelBuilder`
storage and the `Ui` convenience client. The shipped [hello-panels example](../examples/hello-panels/hello.cpp)
is compilable author code, with no private host includes or QML.

## Build, install for development, and open

```sh
cmake -S examples/hello-panels -B hello-out -G Ninja -DCMAKE_PREFIX_PATH=/absolute/sdk
cmake --build hello-out
epictuner --plugin-development --plugin-manifest /absolute/hello-out/plugin.json
```

Keep the generated `plugin.json` beside `hello-panels.dll` (Windows x64) or
`hello-panels.so` (Linux x64/ARM64). For a multi-config generator, copy the
manifest beside the library in `Release`. Installing this M2 development example
means copying this directory and loading its manifest. Persistent unsigned
enablement, signed package installation/update and licenses are M5.

Open **Plugins → Examples/Hello panels**. The example provides editable fields,
a two-way name binding, conditional controls, selectable tabs, results and a chart,
a control gallery, two independent inspectors, and a parent-modal dialog.
Closing/reopening restores opted-in values, selected tabs, splitter ratio, focus,
placement and geometry. Restart only restores compatible visible non-modal panels;
it never repeats a previous button callback. Fault commands exist only when built
with the repository test fixture's `ET_HELLO_TEST_FAULTS` definition.

## Calls, results, and lifetime

All service calls require the worker dispatcher. Background jobs post their
results to that dispatcher before updating UI. Define calls copy the panel,
nodes, property spans and strings before returning; no author pointer crosses
IPC. A definition is immutable and unique for the worker session. The panel ID
must start with the plugin ID plus `.`; node IDs are unique within a definition.
Parents may appear before or after children, but cycles and depth >32 are rejected.

`ET_OK` from a call means **enqueued**, not host acceptance. Pass
`et_async_options` to receive acceptance/rejection through `et_completion`.
The C++ Ui wrapper catches listener exceptions inside the plugin compiler and
returns ET_ERROR_CALLBACK; the worker then fails visibly. C listeners return an
et_result. Completion callbacks must be noexcept and must never throw across the
C ABI (especially across compilers); the example logs rejections explicitly.
Completion context and callback code must remain alive until completion or stop.
Stop cancels pending completions before the plugin's stop callback. An operation
that misses its deadline terminates the worker; it is never retried automatically.

| Operation | Host result |
|---|---|
| `define_panel` | validates the complete definition before registering menu/navigation |
| `open` | handle in `completion.value` (`ET_VALUE_HANDLE`); `OPENED` event follows |
| `patch` | atomic property batch; accepted/current revision in `completion.value` (`ET_VALUE_U64`) |
| `action(FOCUS/CLOSE)` | focuses the existing instance or begins asynchronous close |
| `action(ALLOW_CLOSE/DENY_CLOSE)` | answers the matching `CLOSE_REQUEST.origin`; stale replies fail |

Patch `expected_revision` must equal the latest event or patch-completion
revision. Rejection leaves the complete model unchanged. Conflicts require using
the newer revision and recomputing the patch; do not blindly retry stale changes.
Node IDs remain stable and accepted patches update existing controls in place.
In host 0.8.3+, multiple synchronous patch calls from one UI event callback, using
that event's panel/revision/origin, are queued in call order. Each accepted reply
provides the next call's revision; a failure cancels its dependent calls. New
user edits still conflict. Status patches can safely precede options patches.
Host 0.8.4 also supports the default origin `0` for these same-event calls.
Prefer one batch for dependent controls. Timer/completion callbacks must use a
fresh revision; see [the migration guide](SDK-PRE2.md#several-patches-from-one-event).
Layout topology is immutable for an instance; use visible conditions or a separate
panel definition instead of injecting arbitrary UI source.

Handles include session identity, project generation, slot and generation. Closing,
project replacement, disable and worker replacement invalidate the corresponding
handles. Reopening creates a new handle. Events contain the handle, definition,
instance, node, scalar value, revision and monotonic per-instance origin. Input
previews do not change stored values or revisions. Commit/selection validates before
publishing, increments the revision and sends the accepted value. Programmatic
patches/binding propagation never generate user commits, avoiding feedback loops.
Invalid input leaves the last valid value and an inline error; Enter commits,
Escape restores, and normal text clipboard/Tab behavior comes from ET controls.

## Controls and property contract

**Property IDs and event types are separate domains.** `ET_UI_EVENT_SELECTED`
(legacy `ET_UI_SELECTED`) is an event, never a property. Set a select's index with
`ET_UI_SELECTED_INDEX` / `ET_UI_VALUE` or the C++ `selectedIndex()` helpers.
See the [pre.2 migration example](SDK-PRE2.md) for dynamic options.

The C header lists the exact constants. UI properties accept finite `F64`, `BOOL`
or bounded UTF-8 `STRING` values as appropriate. Other tagged variants are rejected.
Text is plain text; QML, HTML, JavaScript and arbitrary expressions are never
evaluated. Numeric fields use a locale-independent decimal point in this version.

| Node kinds | Rendering and value |
|---|---|
| column, row, grid, section, form | ET spacing; form fields align labels and units; grid has 1–12 columns |
| tabs, splitter, scroll | selected tab index; horizontal two-child splitter ratio 0.1–0.9; bounded nested scrolling |
| label, banner, separator | plain text and themed feedback |
| button | ordered click event on the worker dispatcher |
| text, number | string or finite value with minimum/maximum/decimals; read-only support |
| checkbox, switch | boolean value, keyboard/mouse input and ET theme |
| select | newline-delimited options and zero-based selected index |
| slider, progress | constrained number; progress is 0–1; combine buttons with jobs for Cancel |
| image | package PNG/JPG beside the manifest or under assets/, at most 2 MiB; host constrains decoded display size |
| list, table, tree | bounded data, virtualized rows, filter, selection; table sorts by column; tree expands/collapses |
| chart | at most 512 finite samples, automatically scaled and theme-aware |

Common properties: title, value, enabled, visible, tooltip, error, readOnly, help,
min/preferred/max width and height, fill and stretch. `help` permits HTTPS only,
opened by an explicit user click. Dimensions are logical pixels scaled by the
current ET scale. Chart/data strings remain subject to the 4096-byte string bound.
Table `options` supplies newline-delimited column headings; `data` uses newlines
for rows and tabs for cells. Tree `data` uses leading tabs for indentation. All
selection events report the original data row, independent of sorting/filtering.
These are plugin-owned display datasets; they grant no calibration or ECU access.

### Event value types

The following are input events; programmatic patches do not emit them. All
control kinds are covered below. Lifecycle events (`OPENED`, `CLOSED`,
`CLOSE_REQUEST`, `FOCUSED`, `RESIZED`) carry `ET_VALUE_NULL`; panel/instance and
revision/origin identify their context.

| Control | Input event | `et_ui_event.value` |
|---|---|---|
| button | `ET_UI_CLICK` | `ET_VALUE_NULL` |
| text | `ET_UI_PREVIEW`, `ET_UI_COMMIT` | `ET_VALUE_STRING` |
| number, slider | `ET_UI_PREVIEW`, `ET_UI_COMMIT` | `ET_VALUE_F64` |
| checkbox, switch | `ET_UI_COMMIT` | `ET_VALUE_BOOL` |
| select, tabs | `ET_UI_EVENT_SELECTED` | `ET_VALUE_F64`, zero-based option/tab index |
| list, table, tree | `ET_UI_EVENT_SELECTED` | `ET_VALUE_F64`, original data-row index before filtering/sorting |
| splitter | `ET_UI_COMMIT` | `ET_VALUE_F64`, ratio 0.1–0.9 |
| plugin-owned calibration table, curve, surface | `ET_UI_COMMIT` | `ET_VALUE_STRING`, tab/newline numeric dataset |
| channel picker, analysis panel | `ET_UI_COMMIT` | `ET_VALUE_STRING`, chosen ID/text |
| field editor, host-backed calibration table/curve/surface | host tuning operations | Use tune/read-service events rather than generic UI input |
| column, row, grid, section, form, scroll, label, banner, separator, progress, image, chart, live panel, datalog panel | none | No generic value-input event |

### Property types and control use

The host validates property types on the complete resulting model. A property
irrelevant to a control may be stored but has no rendered effect; it does not
change that control's value type. All properties in a patch must validate.

| Properties | Type | Control use |
|---|---|---|
| title, tooltip, error, help | STRING | Common text; help must be HTTPS |
| enabled, visible, readOnly, persist, twoWay, fill | BOOL | Common state/layout; persist opts a value into storage |
| bindValue, bindEnabled, bindVisible | STRING | Same-panel node IDs; value bindings must have compatible types |
| min/preferred/max width and height, stretch | F64 | Common layout limits; stretch is integral |
| minimum, maximum, step, decimals | F64 | Numeric controls and plugin datasets; decimals is integral 0–8 |
| units | STRING | Numeric/dataset label; never a selected index |
| options | STRING | Select options or table headings, newline delimited; empty select = no options |
| value / selected index | See event table | Corresponding control's typed value; progress F64 0–1, select allows -1 for none |
| columns | F64 | Grid/dataset column count, integral 1–12 |
| data | STRING | List/table/tree/chart or initial plugin dataset |
| source | STRING | Image: bounded adjacent PNG/JPG |
| target | STRING | Prefab host ID; empty target selects a plugin-owned dataset |

For empty select options, `value=-1` is recommended; legacy default `0` is also
accepted, but no item is rendered and no user selection is emitted. Replace
options and index together to avoid retaining an out-of-range old selection.
Rejection details identify the node and property, appear in plugin diagnostics,
and are delivered as the completion's `code` and borrowed `detail` string.

`bindValue`, `bindEnabled` and `bindVisible` name another node in the same instance.
Conditions require boolean values. Value types and destination constraints are
checked, cycles are rejected, and one-way binding is the default. Set `twoWay` for
editable aliases; source read-only/disabled state cannot be bypassed. Channel and
tune sources use the read services and [calibration prefabs](PREFABS.md); local UI-model bindings do not confer access.

## Panels, persistence and failure

### Explicit docked and popout panels

Use a placement mask to say where a definition is allowed to open. A floating
panel uses the same ET controls and prefabs as a workspace panel; it needs no
custom renderer or native window library.

```cpp
PanelBuilder docked("com.example.tool.main", "Calibration workspace");
docked.definition.placements = ET_PLACEMENT_WORKSPACE;
docked.add(ET_UI_COLUMN, "root");
docked.prefab(ET_UI_CALIBRATION_TABLE, "table", "fuelTable", "root");
ui.define(docked, &completion);

PanelBuilder popout("com.example.tool.popout", "Calibration popout");
popout.definition.placements = ET_PLACEMENT_FLOATING;
popout.add(ET_UI_COLUMN, "root");
popout.prefab(ET_UI_CALIBRATION_TABLE, "table", "fuelTable", "root");
ui.define(popout, &completion);

// After both definitions complete successfully, open on explicit user actions:
ui.open("com.example.tool.main", {}, ET_PLACEMENT_WORKSPACE);
ui.open("com.example.tool.popout", {}, ET_PLACEMENT_FLOATING);
```

Declare both definitions and their placements in the signed manifest. This example
uses two singleton definitions, so each can be open and focused independently.
Route events by `event.definition` and keep asynchronous callback/model state
per view. Project-backed prefabs share calibration data; plugin-owned inputs and
UI revisions belong to their view. See the complete
[calibration workbench](../examples/calibration-workbench/README.md).

Alternatively, one definition can allow both placements and multiple instances:

```cpp
panel.definition.placements = ET_PLACEMENT_WORKSPACE | ET_PLACEMENT_FLOATING;
panel.definition.flags |= ET_PANEL_MULTIPLE;
// After successful definition:
ui.open("com.example.tool.main", "Docked", ET_PLACEMENT_WORKSPACE);
ui.open("com.example.tool.main", "Popout", ET_PLACEMENT_FLOATING);
```

In that case route state by `event.instance` and panel handle. Opening the same
instance again focuses its existing window; it does not move it to another
placement. Close then reopen explicitly to change its placement.


`placements` is a mask of Workspace, Floating and Modal. Placement zero chooses
saved state, then the ET pop-outs preference where permitted. Single-instance
panels use an empty instance ID; opening again focuses them. Multiple-instance
definitions require explicit IDs and own independent nodes, values and handles.
All floating/modal windows are ET-managed. Modality is **parent-window modality**.
The GUI never waits synchronously for a plugin callback. Closing a dirty-check
panel sends `CLOSE_REQUEST`; a missing answer closes it after 3000 ms. Worker
failure immediately releases modal windows and disables other controls with a
recovery message. Disable closes all windows and drains the worker.

`ET_PANEL_PROJECT` restricts open/restore to a current project. Closing/replacing
that project invalidates its panels. `ET_PANEL_PERSIST` saves state under the
plugin/panel/instance, schema and optional project identity. Individual node values
require `persist=true`; sensitive values should not opt in. Schema changes start
with new state. Restart restoration only occurs after explicitly loading an enabled,
compatible development plugin. Licensing eligibility will be added at M5.
Windows are clamped onto available screens at creation/screen changes, including
positions saved on a removed monitor. Placement/geometry, workspace and internal
tab selection, splitter values and last input focus are retained.

Per worker: 32 definitions, 64 live instances, 4096 total definition nodes and
4096 live nodes, depth 32, 32 pending requests, 30 UI calls/second. Persistence
retains at most 64 identities per plugin. A serialized message must fit 64 KiB
(including envelope); large definitions must use separate panels. Batch display
patches at or below 30 Hz; Qt coalesces painting and input previews are debounced.
Overflow returns `ET_ERROR_LIMIT`; it does not silently truncate or apply part of
a batch. Revisions/origins/slots never wrap into reusable identities.

## Acceptance boundaries

Windows x64 native Qt Quick UI is the M2 acceptance target on this workstation.
Compiler/OS-specific results are recorded in the repository's M2 audit. Linux
GUI, real second-monitor hot-plug, and physical Pi display/input certification
require their respective environments. M4 supplies calibration prefabs and
host edit services; M5 supplies signed installation/licensing; M6 supplies
independent custom windows. None are implied by the host-rendered M2 panels.
