Skip to main content

reactor-input

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.

소스 정보

저장소
microsoft/microsoft-ui-reactor
최근 소스 활동
2026년 9월 24일 20:38
감지된 SKILL.md 언어
영어
스타
674
포크
58

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
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)
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기