| name | reactor-input |
|---|---|
| description | Input handling and gesture recognition in Reactor — pointer events, tap, keyboard, pan/pinch/rotate gestures, long press, focus management, access keys, and drag-and-drop with typed payloads. Load this when building interactive UIs with gesture recognition, drag-drop, or custom input handling. |
Reactor exposes input via trampoline-dispatched .On* modifiers. Events
auto-enable their underlying WinUI flags (ManipulationMode, AllowDrop,
etc.) when you attach a handler, so you never need to set those manually.
| API | Purpose |
|---|---|
.OnPointerEntered/Exited/Pressed/Released/Moved |
Pointer events |
.OnTapped() / .OnDoubleTapped() / .OnRightTapped() |
Tap events |
.OnKeyDown() / .OnKeyUp() |
Keyboard events |
.OnPan(...) |
Pan gesture (drag with inertia) |
.OnPinch(...) |
Pinch-to-zoom gesture |
.OnRotate(...) |
Rotation gesture |
.OnLongPress(...) |
Press-and-hold |
.OnGotFocus() / .OnLostFocus() |
Focus change events |
UseElementFocus() |
Untyped ref + dispatcher-scheduled focus |
UseElementRef<T>() |
Typed element ref — .Current is T (no cast) |
.AccessKey("S") |
Alt+key keyboard shortcut |
.OnDragStart(...) / .OnDrop(...) |
Drag-and-drop |
var (isHovered, setIsHovered) = UseState(false);
return Border(child)
.OnPointerEntered((s, e) => setIsHovered(true))
.OnPointerExited((s, e) => setIsHovered(false))
.Background(isHovered ? Theme.SubtleFill : Colors.Transparent);Events: OnPointerEntered, OnPointerExited, OnPointerPressed,
OnPointerReleased, OnPointerMoved, OnPointerCanceled.
The (s, e) signature gives you the sender and PointerRoutedEventArgs.
Border(child)
.OnTapped((s, e) => HandleClick())
.OnDoubleTapped((s, e) => HandleDoubleClick())
.OnRightTapped((s, e) => ShowContextMenu())TextField(text, setText)
.OnKeyDown((s, e) =>
{
if (e.Key == VirtualKey.Enter)
{
Submit();
e.Handled = true;
}
})Gestures follow a phase lifecycle: Began → Changed (repeat) → Ended | Cancelled.
var (offset, setOffset) = UseState(new Point(0, 0));
return Border(child)
.OnPan(
minimumDistance: 10, // pixels before gesture starts
axis: PanAxis.Both, // or Horizontal, Vertical
withInertia: true, // momentum after release
onBegan: (e) => { /* pan began */ },
onChanged: (e) =>
{
setOffset(new Point(
offset.X + e.Delta.Translation.X,
offset.Y + e.Delta.Translation.Y));
},
onEnded: (e) => { /* pan ended */ })
.Translation((float)offset.X, (float)offset.Y, 0);For smooth dragging, write Translation directly via ref during the
gesture and only setState at the end:
var itemRef = UseRef<UIElement>();
var (finalPos, setFinalPos) = UseState(new Point(0, 0));
return Border(child)
.Ref(itemRef)
.OnPan(
onChanged: (e) =>
{
// Direct property write — no re-render per frame
if (itemRef.Current is { } el)
{
var t = el.Translation;
el.Translation = new System.Numerics.Vector3(
t.X + (float)e.Delta.Translation.X,
t.Y + (float)e.Delta.Translation.Y,
t.Z);
}
},
onEnded: (e) =>
{
// Sync state once at end
if (itemRef.Current is { } el)
setFinalPos(new Point(el.Translation.X, el.Translation.Y));
});var (scale, setScale) = UseState(1.0f);
return Border(child)
.OnPinch(
onChanged: (e) => setScale(scale * (float)e.Delta.Scale),
onEnded: (e) => { })
.Scale(scale);
// Rotation:
var (angle, setAngle) = UseState(0f);
return Border(child)
.OnRotate(
onChanged: (e) => setAngle(angle + (float)e.Delta.Rotation),
onEnded: (e) => { })
.Rotation(angle);Touch/pen has built-in long press; mouse requires emulation:
Border(child)
.OnLongPress(
onTriggered: (e) => ShowEditMode(),
enableMouseEmulation: true) // also trigger on mouse press-and-holdvar focusRef = UseElementFocus();
return VStack(12,
TextField(text, setText).Ref(focusRef.Ref),
Button("Focus the field", () => focusRef.RequestFocus())
);UseElementFocus returns a handle with .Ref (attach to the element)
and .RequestFocus() (imperatively focus it).
When you actually need to call methods on the underlying control (SelectAll()
on a TextBox, Focus(FocusState.Programmatic), etc.), use the typed variant.
.Current is already typed as T — no cast at the call site:
var inputRef = UseElementRef<TextBox>();
UseEffect(() => inputRef.Current?.SelectAll(), Array.Empty<object>());
return TextField(query, setQuery).Ref(inputRef);The constraint is T : FrameworkElement. In DEBUG builds Reactor asserts
the actual mounted element is a T; in release the mismatch is silent and
.Current returns null. Spec 033 §3.
TextField(text, setText)
.OnGotFocus((s, e) => ShowSuggestions())
.OnLostFocus((s, e) => HideSuggestions())Button("Save", onSave).AccessKey("S") // Alt+STextField(name, setName).IsTabStop(true).TabIndex(1)
TextField(email, setEmail).IsTabStop(true).TabIndex(2)
Button("Submit", onSubmit).IsTabStop(true).TabIndex(3)Reactor's drag-drop system supports typed in-process payloads (via a transfer registry) and standard formats for cross-process drops.
// Drag source — provide a DragData factory
Border(TextBlock(item.Name))
.OnDragStart(
getData: () => DragData.Typed(item),
allowedOperations: DragOperations.Copy | DragOperations.Move)
// Drop target — typed overload auto-extracts payload
Border(TextBlock("Drop here"))
.OnDrop<Border, Item>(
onDrop: (droppedItem) => HandleDrop(droppedItem))For type-safe object transfer within the same app, use the typed
OnDragStart<T, TPayload> and OnDrop<T, TPayload> overloads:
// Drag source — typed payload via getPayload factory
Border(TextBlock(item.Name))
.OnDragStart<Border, Item>(
getPayload: () => item,
allowedOperations: DragOperations.Move,
onEnd: (ctx) =>
{
if (ctx.CompletedOperation == DragOperations.Move && !ctx.WasCancelled)
RemoveItem(item); // only remove after confirmed drop
})
// Drop target — typed extraction
Border(TextBlock("Drop here"))
.OnDrop<Border, Item>(
onDrop: (droppedItem) => MoveItem(droppedItem),
acceptedOps: DragOperations.Move)For cross-process drops or custom handling:
Border(TextBlock("Drop here"))
.OnDragOver((args) =>
{
args.UIOverride.Caption = "Copy here";
args.UIOverride.IsCaptionVisible = true;
})
.OnDrop((args) =>
{
if (args.Data.TryGetTypedPayload<Item>(out var item))
HandleDrop(item);
})Via args.UIOverride on drag-over handlers:
.OnDragOver((args) =>
{
args.UIOverride.IsCaptionVisible = true;
args.UIOverride.IsContentVisible = true;
args.UIOverride.IsGlyphVisible = false;
args.UIOverride.Caption = "Move to folder";
})- CANNOT combine
OnPanandOnTappedon the same element. They conflict viaManipulationMode. If you need both, putOnTappedon a child andOnPanon the parent. - 60Hz pan pattern — for smooth drag UIs, write
Translationdirectly via ref duringonChanged, and onlysetStateinonEnded. - Gesture phase lifecycle — always handle both
onEndedandonCancelledto avoid stuck state. AllowDropauto-enables — just attaching.OnDrop()sets the flag. Don't set it manually.- Typed payloads are in-process only —
TryGetTypedPayload<T>returns false for cross-app drops. Always check the return value. - Use
e.Handled = trueon keyboard events to prevent bubbling when you've handled the key. - Mouse long-press needs
enableMouseEmulation: true— touch/pen have native hold detection, but mouse does not.