| name | reactor-input |
| description | Reactor input — gestures (`OnPan`, `OnPinch`, `OnRotate`), pointer events, drag-and-drop (`OnDragStarting`), focus management (`UseElementFocus`, `UseFocus`, `UseFocusTrap`). Use when implementing direct manipulation or custom focus behavior. |
Input and Gestures in Reactor
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.
Controlled prop note: input factories keep the ergonomic
(value, setter) shape. The underlying controlled element properties are
Optional<T> so Unset can mean "control-owned"; direct reads need
.Value or .GetValueOrDefault(...). See
migration/050-optional-t.md.
Quick reference
| API | Purpose |
|---|
.OnPointerEntered/Exited/Pressed/Released/Moved | Pointer events |
.IsHitTestVisible(bool) | Opt an element + its subtree out of hit-testing |
.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) |
.XYFocusUp/Down/Left/Right(ref) | Directional focus reference props |
.AccessKey("S") | Alt+key keyboard shortcut |
.OnDragStart(...) / .OnDrop(...) | Drag-and-drop |
1. Pointer events
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.
Opting out of hit-testing
.IsHitTestVisible(false) sets UIElement.IsHitTestVisible — the element
and its subtree stop being pointer targets, so decorative or overlay
layers let pointer events pass through to whatever is underneath.
Border(overlayGlow).IsHitTestVisible(false)
Unlike .IsEnabled(false), it does not grey the control out or block focus —
reach for .IsEnabled(false) when you want a disabled affordance, and
.IsHitTestVisible(false) when you only want clicks to fall through.
2. Tap events
Border(child)
.OnTapped((s, e) => HandleClick())
.OnDoubleTapped((s, e) => HandleDoubleClick())
.OnRightTapped((s, e) => ShowContextMenu())
3. Keyboard events
TextBox(text, setText)
.OnKeyDown((s, e) =>
{
if (e.Key == VirtualKey.Enter)
{
Submit();
e.Handled = true;
}
})
4. Continuous gestures (Pan, Pinch, Rotate)
Gestures follow a phase lifecycle: Began → Changed (repeat) → Ended | Cancelled.
Pan gesture
var (offset, setOffset) = UseState(new Point(0, 0));
return Border(child)
.OnPan(
minimumDistance: 10,
axis: PanAxis.Both,
withInertia: true,
onBegan: (e) => { },
onChanged: (e) =>
{
setOffset(new Point(
offset.X + e.Delta.Translation.X,
offset.Y + e.Delta.Translation.Y));
},
onEnded: (e) => { })
.Translation((float)offset.X, (float)offset.Y, 0);
60Hz pan pattern (performance-critical)
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) =>
{
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) =>
{
if (itemRef.Current is { } el)
setFinalPos(new Point(el.Translation.X, el.Translation.Y));
});
Pinch and Rotate
var (scale, setScale) = UseState(1.0f);
return Border(child)
.OnPinch(
onChanged: (e) => setScale(scale * (float)e.Delta.Scale),
onEnded: (e) => { })
.Scale(scale);
var (angle, setAngle) = UseState(0f);
return Border(child)
.OnRotate(
onChanged: (e) => setAngle(angle + (float)e.Delta.Rotation),
onEnded: (e) => { })
.Rotation(angle);
5. Long press
Touch/pen has built-in long press; mouse requires emulation:
Border(child)
.OnLongPress(
onTriggered: (e) => ShowEditMode(),
enableMouseEmulation: true)
6. Focus management
UseElementFocus
var focusRef = this.UseElementFocus();
return VStack(12,
TextBox(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).
UseElementRef<T> — typed refs
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 = this.UseElementRef<TextBox>();
UseEffect(() => inputRef.Current?.SelectAll(), Array.Empty<object>());
return TextBox(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.
XYFocus reference props
Use XYFocus refs for gamepad/keyboard directional navigation graphs.
The target can mount before or after the referrer; Reactor rewrites the
WinUI UIElement.XYFocus* property when the referenced element becomes
available and clears it when that element unmounts.
var left = this.UseElementRef<FrameworkElement>();
var right = this.UseElementRef<FrameworkElement>();
return HStack(8,
Button("Left").Ref(left).XYFocusRight(right),
Button("Right").Ref(right).XYFocusLeft(left));
For cyclic focus rings, declare both directions. Do not assign
button.XYFocusRight = right.Current in a handler; that snapshot will
not survive late mount or recreation.
Focus events
TextBox(text, setText)
.OnGotFocus((s, e) => ShowSuggestions())
.OnLostFocus((s, e) => HideSuggestions())
Access keys (keyboard shortcuts)
Button("Save", onSave).AccessKey("S")
Tab order
TextBox(name, setName).IsTabStop(true).TabIndex(1)
TextBox(email, setEmail).IsTabStop(true).TabIndex(2)
Button("Submit", onSubmit).IsTabStop(true).TabIndex(3)
7. Drag and drop
Reactor's drag-drop system supports typed in-process payloads (via a
transfer registry) and standard formats for cross-process drops.
Basic drag source + drop target
Border(TextBlock(item.Name))
.OnDragStart(
getData: () => DragData.Typed(item),
allowedOperations: DragOperations.Copy | DragOperations.Move)
Border(TextBlock("Drop here"))
.OnDrop<BorderElement, Item>(
onDrop: (droppedItem) => HandleDrop(droppedItem))
Typed in-process payloads
For type-safe object transfer within the same app, use the typed
OnDragStart<T, TPayload> and OnDrop<T, TPayload> overloads:
Border(TextBlock(item.Name))
.OnDragStart<BorderElement, Item>(
getPayload: () => item,
allowedOperations: DragOperations.Move,
onEnd: (ctx) =>
{
if (ctx.CompletedOperation == DragOperations.Move && !ctx.WasCancelled)
RemoveItem(item);
})
Border(TextBlock("Drop here"))
.OnDrop<BorderElement, Item>(
onDrop: (droppedItem) => MoveItem(droppedItem),
acceptedOps: DragOperations.Move)
Untyped drop target (raw DragTargetArgs)
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);
})
DragUI customization
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";
})
Critical gotchas
- CANNOT combine
OnPan and OnTapped on the same element. They
conflict via ManipulationMode. If you need both, put OnTapped on a
child and OnPan on the parent.
- 60Hz pan pattern — for smooth drag UIs, write
Translation directly
via ref during onChanged, and only setState in onEnded.
- Gesture phase lifecycle — always handle both
onEnded and
onCancelled to avoid stuck state.
AllowDrop auto-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 = true on 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.