SyncroMesh: 0.5.0 → 0.6.0 migration
Upgrade SyncroMesh raw mouse/touch APIs to virtual interactions, capture, gestures, and explicit cancellation.
This guide upgrades SyncroMesh applications from 0.5.0 to 0.6.0. The input replacement also applies to scripts using the 0.5.1 raw-input API. Version 0.6.0 removes the old API without deprecated adapters: update application code, bundled standard-library assets, TypeScript declarations, and native integrations together.
A release outside the original element still finishes the owned interaction. Remove mouse-exit release workarounds and always handle cancel. Cancellation is cleanup, never a successful click or drop.
1. Replace handlers by intent #
| 0.5.0 API / pattern | 0.6.0 replacement |
|---|---|
| onMouseClick / setOnMouseClick | onActivate / setOnActivate for a qualified click or tap. |
| onMouseDown / setOnMouseDown | onPress / setOnPress with phase === 'start' for held state. |
| onMouseUp / setOnMouseUp | onPress with phase === 'end' or 'cancel' to clear held state; use onActivate for actions. |
| onMouseMove / setOnMouseMove | onHover with move for hover movement; onPress.change for held movement; onDrag for threshold-recognized dragging. |
| onMouseEnter / onMouseExit and their setters | onHover / setOnHover with enter/leave phases; leave does not release a press. |
| onMouseScroll / setOnMouseScroll | onScroll / setOnScroll with event.scroll:{x,y}, event.unit and event.scrollSource. |
| Event.on(mouseDown/mouseUp/mouseMove/mouseScroll) | Await Helix/Input.setHandlers(windowId, handlers) after creating the window. |
Element creation uses onPress, onActivate, onHover, onDrag, onScroll, onLongPress, onDoubleActivate, and onTransform. Low-level setters take (windowId, elementId, handler); wrapper methods take just the handler. Pass null to an element setter to clear it. All runtime registrations are promise-based; await them.
Before: positional callbacks #
// 0.5.0
import * as UI from 'Helix/UserInterface';
await UI.setOnMouseClick(windowId, elementId, (window, local, button) => {
performAction(local);
});After: one typed event object #
// 0.6.0: primary mouse click or touch tap by default.
import * as UI from 'Helix/UserInterface';
await UI.setOnActivate(windowId, elementId, (event) => {
performAction(event.local);
});
await UI.setOnHover(windowId, elementId, (event) => {
if (event.phase === 'enter') showHover();
if (event.phase === 'leave') hideHover();
});2. Replace manual capture and drag state #
onPress, onDrag, and onTransform use start/change/end/cancel. A started lifecycle has exactly one terminal event while its callback runtime is live. Ownership persists outside the element and, when native capture is available, outside the window. Capture loss cancels instead of silently dropping the release. Use interactionId as a map key so simultaneous fingers/buttons do not clear each other. Runtime disposal suppresses callbacks, so also release application resources during shutdown.
const held = new Map();
await UI.setOnPress(windowId, elementId, (event) => {
if (event.phase === 'start') held.set(event.interactionId, event.start);
if (event.phase === 'end' || event.phase === 'cancel') {
held.delete(event.interactionId);
}
renderHeldState(held.size > 0);
});
await UI.setOnDrag(windowId, elementId, (event) => {
if (event.phase === 'start') beginDrag(event.interactionId, event.start);
if (event.phase === 'change') updateDrag(event.interactionId, event.position);
if (event.phase === 'end') commitDrag(event.interactionId, event.position);
if (event.phase === 'cancel') discardDrag(event.interactionId, event.reason);
});Drag begins after its movement threshold; event.start retains the original press position. An eligible ancestor recognizer can take ownership and cancel the previous press with reason: superseded. Replacing handlers/options, destroying/disabling/detaching an owner, losing focus/device/window/capture, or handing input to OS drag/drop also cancels active interactions. Pending events retain their original callback registration; do not assume a replacement handler receives an old interaction’s terminal event.
3. Update event fields and scroll handling #
| Field | Meaning / migration |
|---|---|
| position, start, delta, displacement | Logical-window coordinates: current position, original start, sample movement, and movement from the start. |
| canvas, local | UI coordinates; local is relative to the current owner and can be negative outside it. Replace positional local arguments with event.local. |
| windowId, targetId, ownerId | Window identity, hit target, and interaction owner; target and owner can differ after ancestor takeover. |
| interactionId, pointerIds | Opaque strings. Keep them as strings; do not parse them as native device IDs or assume one mouse contact. |
| button, source | Semantic primary/secondary/middle/back/forward/other button and mouse/touch source; replace platform-specific numeric button checks. |
| modifiers, timestamp | Modifier snapshot and monotonic milliseconds. Do not compare timestamp with Date.now() or simulation seconds. |
| scroll, unit, scrollSource | Two axes; positive x/y means right/down. Units are pixel or step. Replace old scalar delta/sign assumptions and choose application scaling for each unit. |
| pressure, contactSize, contactOrientation | Optional device metadata; handle absence. Contact size uses logical-window units, orientation uses radians clockwise from positive Y. |
await UI.setOnScroll(windowId, elementId, (event) => {
const pixelsPerUnit = event.unit === 'step' ? 40 : 1;
scrollContent({
x: event.scroll.x * pixelsPerUnit,
y: event.scroll.y * pixelsPerUnit
});
});4. Register window input and recognition options #
Import Helix/Input for window handlers. setHandlers replaces the complete handler set for the calling script; include every handler you want to retain and use {} to unregister. Window/game input receives only interactions unclaimed by UI. Recipients are fixed when the interaction starts, so moving over UI does not steal an ongoing game drag. Hover-only UI targets do not claim presses; set blockInput: true for a hit target that should block input without an action handler. Callback return values do not control native routing.
import * as Input from 'Helix/Input';
// windowId comes from your existing window creation.
await Input.setOptions(windowId, {
buttons: ['primary', 'secondary'],
mouseTolerance: 6,
touchTolerance: 10,
longPressMs: 500,
doubleActivateMs: 300,
dragAxis: 'any'
});
await Input.setHandlers(windowId, {
onActivate(event) {
if (event.button === 'primary') selectAt(event.position);
if (event.button === 'secondary') openMenuAt(event.position);
},
onDrag(event) {
if (event.phase === 'change') updateSelection(event.start, event.position);
if (event.phase === 'end' || event.phase === 'cancel') clearSelectionPreview();
}
});
// During application cleanup:
// await Input.setHandlers(windowId, {});Defaults are primary-only buttons, mouse tolerance 6, touch tolerance 10, long press 500 ms, double activation 300 ms, unrestricted drag axis, and no input blocking. Tolerances use logical-window units. Element inputOptions or UI.setInputOptions(windowId, elementId, options) override specified fields from the window defaults. Use dragAxis: 'x' or 'y' for directional recognizers. Options changes cancel active interactions.
Board ownership and world coordinates #
Register from the orchestrator or attached board context that should own the handlers. Pass the relevant window ID to board scripts rather than relying on a global mouse event bus. Only attached boards participate; detaching/stopping a board cancels its owned interactions and newly attached boards do not join an already-started interaction. Window options are shared defaults for the physical window, so coordinate changes between scripts.
Window handlers receive world projected through the current camera/attachment and startWorld frozen at interaction start. For world-space selections use startWorld and world; for screen-space gestures use start and position. Replace the old global payload’s event point with position; world remains optional, so check it before issuing world commands.
await Input.setHandlers(windowId, {
onActivate(event) {
if (event.world) selectWorldPoint(event.world);
},
onDrag(event) {
if (event.phase === 'change' && event.world && event.startWorld) {
previewWorldSelection(event.startWorld, event.world);
}
if (event.phase === 'end' || event.phase === 'cancel') clearSelectionPreview();
}
});5. Adopt gestures deliberately #
- Long press:
onLongPressfires only while inside and within tolerance. Once it wins, the interaction cannot later activate or start a drag. - Double activation: register
onDoubleActivatefor the second qualified activation. Both single activations fire immediately; the double event does not replace or delay them. Design single/double actions accordingly. - Transform: register
onTransformon the eligible common ancestor for two-touch pan/pinch/rotation. Use cumulativetranslation,scale, androtationin radians. The earliest two contacts define the gesture; extra contacts and surviving contacts remain consumed until release. - Keyboard, text, custom events, and OS data drag/drop: keep their existing separate APIs. Virtual
onDragis an application gesture, not OS MIME-data drag/drop.
6. Rebuild native integrations #
The KoyaUiMouseMoveCallback, KoyaUiMouseButtonCallback, KoyaUiMouseScrollCallback, and koya_ui_set_on_mouse_* functions have been removed. Include helix_input.h, use HelixInputCallback(const HelixInputEvent*, void*), and register element handlers with koya_ui_set_input_handler(windowId, elementId, HELIX_INPUT_*, callback, userData). Use koya_ui_set_input_options for elements and helix_input_set_handlers / helix_input_set_options for window registration. Rebuild consumers of the changed headers; the old binary signatures are incompatible.
Native event string and array pointers are borrowed for the callback only; copy them before queueing work. Initialize all option fields you specify: native buttons are a semantic bitmask, dragAxis is a, x, or y, and specified == 0 means every option field is supplied. For partial overrides, use the field masks documented in the header. Available metadata is indicated by the event’s metadata flags.
Upgrade verification #
- Search your application for
onMouse,setOnMouse, removed global mouse/pointer event listeners, andkoya_ui_set_on_mouse_calls. - Verify click/tap, release outside the element/window, focus loss, owner destruction, handler replacement, and cancellation during a drag.
- Verify multiple simultaneous fingers/buttons, hover independent of held state, ancestor takeover, and UI blocking of window/game actions.
- Verify both scroll axes, wheel versus pixel scaling, custom button filters, long press, double activation, and two-touch transforms.
See Helix/Input for the full virtual-event contract and Changelog for the 0.6.0 release notes.
Optional native scrolling #
Existing virtual onScroll handlers still receive raw deltas when native scrolling is disabled. To adopt automatic movement and clipping, configure scroll: true or axis/options and give content explicit overflow bounds. Treat onScroll as an observer once enabled: remove manual translations and inertia timers. Native notifications add nativeScroll, lifecycle phase, coasting, scrollOffset, scrollLimit, and velocity. stopScroll cancels the lifecycle while end reports natural settlement. See Native scrolling.