- 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`](../../../../docs/guide/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
<!-- index:pointer-input -->
Pointer handlers attach like any other modifier; Reactor installs one stable
trampoline per element, so re-rendering with a fresh closure costs no
re-subscription.
```csharp
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.
```csharp
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` and other non-control elements are not tab stops by default: a
`Border` with `.OnTapped(...)` answers the mouse but is skipped by the
keyboard. Add `.IsTabStop(true)` and an `.OnKeyDown` Enter/Space handler, or
use a `Button`, whenever the gesture is a real command.
```csharp
Border(child)
.OnTapped((s, e) => HandleClick())
.OnDoubleTapped((s, e) => HandleDoubleClick())
.OnRightTapped((s, e) => ShowContextMenu())
```
<!-- /index:pointer-input -->
## 3. Keyboard events
<!-- index:keyboard-input -->
Attach `.OnKeyDown` / `.OnKeyUp` to any element. The handler receives
`(sender, KeyRoutedEventArgs)`; set `e.Handled = true` to stop the event
bubbling. `.OnPreviewKeyDown` / `.OnPreviewKeyUp` run first, during the
tunnelling phase, and can swallow a key before the focused control sees it.
```csharp
TextBox(text, setText)
.OnKeyDown((s, e) =>
{
if (e.Key == VirtualKey.Enter)
{
Submit();
e.Handled = true;
}
})
```
**`e.Key` is `Windows.System.VirtualKey` — WinRT, not WPF.** It is not
`System.Windows.Input.Key`, and the member names differ: `VirtualKey` has **no**
`OemPeriod`, `OemPlus`, `OemMinus` or `Equal`. Punctuation and OEM keys simply
have no named member. Two ways to handle them:
```csharp
// 1. Compare the numeric virtual-key code for OEM keys.
.OnKeyDown((s, e) =>
{
if ((int)e.Key == 190) AppendDecimalPoint(); // VK_OEM_PERIOD '.'
if ((int)e.Key == 187) Evaluate(); // VK_OEM_PLUS '='
})
// 2. Better for text: let WinUI resolve the character for you.
.OnCharacterReceived((s, e) =>
{
if (e.Character == '.') AppendDecimalPoint();
if (e.Character == '=') Evaluate();
})
```
Named members you *can* rely on are the letters (`VirtualKey.A`), digits
(`VirtualKey.Number0`, `VirtualKey.NumberPad0`), the numpad operators
(`Add`, `Subtract`, `Multiply`, `Divide`, `Decimal`), and the editing and
navigation keys (`Enter`, `Space`, `Back`, `Escape`, `Tab`, `Delete`,
`Left`, `Right`, `Up`, `Down`).
**Ctrl/Alt chords do not belong on `.OnKeyDown`.** A handler on a `TextBox`
only fires while that field has focus, so an app-wide shortcut written that way
silently does nothing elsewhere. Use a `Command` with an `Accelerator`, which
registers with WinUI's accelerator infrastructure. The `REACTOR_INPUT_001`
analyzer flags the mistake.
```csharp
new Command
{
Label = "Save",
Execute = Save,
Accelerator = Accelerator(VirtualKey.S, VirtualKeyModifiers.Control),
}
```
Mind the **scope**, though: an accelerator fires when the surface carrying it is
in the focused element's ancestor chain, so `Button(save)` on its own covers the
button, not the window. `CommandHost([save], subtree)` widens it to a whole
subtree, and a `MenuBar` or `CommandBar` at the window root is what makes a chord
genuinely window-wide — those attach to the window's `KeyboardAccelerators`
collection. See [commanding](../../../../docs/guide/commanding.md).
<!-- /index:keyboard-input -->
## 4. Continuous gestures (Pan, Pinch, Rotate)
<!-- index:pointer-input -->
Gestures follow a **phase lifecycle**: `Began → Changed (repeat) → Ended | Cancelled`.
Attaching a gesture handler auto-enables the underlying `ManipulationMode`, so
you never set that yourself.
### Pan gesture
```csharp
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) =>
{
// PanGesture.Delta is the per-event movement, a Point — Reactor flattens
// WinUI's ManipulationDelta, so there is no e.Delta.Translation here.
setOffset(new Point(offset.X + e.Delta.X, offset.Y + e.Delta.Y));
},
onEnded: (e) => { /* pan ended */ })
.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. The cell must be an **`ElementRef`**
from `UseElementRef<T>` — that is what the reconciler populates. A `UseRef`
box is not accepted by `.Ref(...)` and would never point at anything.
```csharp
var itemRef = this.UseElementRef<FrameworkElement>();
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.X,
t.Y + (float)e.Delta.Y,
t.Z);
}
},
onEnded: (e) =>
{
// Sync state once at end
if (itemRef.Current is { } el)
setFinalPos(new Point(el.Translation.X, el.Translation.Y));
});
```
### Pinch and Rotate
Both gestures carry an absolute value and a per-event delta as flat doubles —
`Scale`/`ScaleDelta` and `Angle`/`AngleDelta`. There is no nested `e.Delta`
object on these two.
```csharp
var (scale, setScale) = UseState(1.0f);
return Border(child)
.OnPinch(
onChanged: (e) => setScale(scale * (float)e.ScaleDelta),
onEnded: (e) => { })
.Scale(scale);
// Rotation:
var (angle, setAngle) = UseState(0f);
return Border(child)
.OnRotate(
onChanged: (e) => setAngle(angle + (float)e.AngleDelta),
onEnded: (e) => { })
.Rotation(angle);
```
## 5. Long press
Touch/pen has built-in long press; mouse requires emulation:
```csharp
Border(child)
.OnLongPress(
onTriggered: (e) => ShowEditMode(),
enableMouseEmulation: true) // also trigger on mouse press-and-hold
```
<!-- /index:pointer-input -->
## 6. Focus management
<!-- index:element-refs -->
An `ElementRef` is the bridge from the declarative tree to a realized WinUI
element. Attach it with `.Ref(...)`, then read `.Current` — it is `null` until
the element mounts, survives re-renders, and clears on unmount, so the same
cell reliably points at whatever is mounted now.
### UseElementFocus
```csharp
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). It is a named tuple, so the
deconstructed form is equivalent and is what the guide uses — `RequestFocus`
schedules the focus on the UI dispatcher, so it is safe to call from an effect
that runs before the element has mounted:
```csharp
var (inputRef, requestFocus) = this.UseElementFocus();
UseEffect(() => requestFocus(), Array.Empty<object>()); // focus on mount
return TextBox(name, setName).AutomationName("Name").Ref(inputRef);
```
For a synchronous attempt, or one whose success you need to observe, call
`Microsoft.UI.Reactor.Input.FocusManager.Focus(inputRef)` or
`FocusManager.FocusAsync(inputRef)` instead.
### 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:
```csharp
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.
```csharp
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. The same rule covers every reference
prop — `TeachingTip(..., target: ref)`, `.LabeledBy(ref)`, `.DescribedBy(refs)`,
`.FlowsTo(refs)` — and `REACTOR_REF_001` flags reading `.Current` to set one.
<!-- /index:element-refs -->
### Focus events
```csharp
TextBox(text, setText)
View on GitHub