Files
integration/Docs/Keyboard-Focus-and-Input-Modes.md

7.8 KiB

Keyboard Focus and Input Modes

Luprex provides a "window management system" that takes full ownership of: keyboard focus, mouse capture, pointer visibility, enhanced input routing, and input mode. Top-level widgets don't call AddToViewport or configure input modes directly — instead, they are added to a single root canvas, and the player controller arbitrates the per-tick state based on which widget is in front.

Key types

  • AlxPlayerControllerBase — owns the top-level RootWidget, builds the input stack, and reconciles focus / capture / pointer state every tick.
  • UlxRootCanvasPanel — the one UCanvasPanel subclass that lives inside RootWidget. Every top-level UI widget in the game is a child of this canvas.
  • UlxRootCanvasSlot — the slot type used for children of UlxRootCanvasPanel. Extends UCanvasPanelSlot with input-mode fields. A widget's slot carries both layout config (anchors, offsets, ZOrder) and its window-management declarations.

These types collapse what would otherwise be two parallel systems (layout + input-mode requests) into one. Adding a widget to the canvas = declaring its window-management intent.

The core rule: ZOrder is authoritative

The widget with the highest ZOrder wins everything. It dictates:

  • Whether the mouse pointer is shown.
  • Whether the viewport captures the mouse.
  • Which widget (or sub-widget) receives keyboard focus.
  • Which input components end up on top of the input stack.

Draw order and input priority are the same fact. A widget you can see on top is the one that gets input.

Slot fields (UlxRootCanvasSlot)

Inherited from UCanvasPanelSlot:

  • ZOrder (int32) — Higher = drawn on top / higher input priority. Can be updated at runtime via Slot->SetZOrder(N).
  • Anchors, Offsets, Alignment — standard UMG canvas layout.

Added by UlxRootCanvasSlot:

  • ShowPointer (bool) — When this widget is in front, the mouse cursor is visible and the viewport releases capture. When false, the cursor is hidden and the viewport captures the mouse (game-style input).
  • BlockInput (bool) — When this widget's input component is pushed onto the stack, it blocks lower-priority components from receiving events.
  • EnableEnhancedInput (bool, default true) — Whether this widget's UInputComponent is included on the input stack at all. Corresponds to "Enhanced Input events fire on this widget."

One property that lives on the widget rather than the slot:

  • UUserWidget::DesiredFocusWidget — the sub-widget to receive keyboard focus when this widget is in front. Null means "focus the viewport."

API

Adding a widget

// Static, BP-callable with DefaultToSelf + HideSelfPin,
// so in Blueprint it looks like a method on UUserWidget.
AlxPlayerControllerBase::AddWidgetToRoot(UUserWidget *Widget);

Adds the widget as a child of the player controller's RootCanvas at ZOrder 0 with default flags. If the widget is already a child, does nothing.

Updating a widget's settings

UlxRootCanvasPanel::SetWidgetWindowManagement(
    UUserWidget *Widget,
    bool ShowPointer, bool BlockInput, bool EnableEnhancedInput,
    bool BringToFront, UWidget *DesiredFocusWidget);

Updates an already-added widget's slot. Writes all the input-mode flags, sets DesiredFocusWidget, and optionally bumps ZOrder to GetMaxZOrder() + 1 if BringToFront is true. Logs an error if the widget isn't yet a root widget.

Forcing a focus re-apply

AlxPlayerControllerBase::RestoreFocusToFrontWidget(const UObject *Context);

Clears LastWidgetGrantedFocus, which causes the next tick of UpdateInputMode to re-apply focus based on the current front widget. Use after mutating DesiredFocusWidget or when a modal is dismissed and focus should return to the underlying widget.

Per-tick flow

On every tick, the player controller runs two passes:

BuildInputStack

Assembles the list of UInputComponents that will receive input events this frame:

  1. Controlled pawn's input component(s).
  2. Level script actors.
  3. The player controller's own input component.
  4. CurrentInputStack (anything pushed via PushInputComponent, filtered to exclude input components already managed by a root canvas slot).
  5. Root canvas widget input components, pushed in ZOrder-ascending order (so highest ZOrder ends up on top of the stack, which gets processed first).

Each widget-managed IC has its bBlockInput flag set from its slot's BlockInput before being pushed.

UpdateInputMode

Reads the front widget's slot and applies the declared state:

  1. Sorts the canvas slots by ZOrder; picks the front widget.
  2. Calls SetShowMouseCursor(Slot->ShowPointer).
  3. Configures viewport capture mode based on ShowPointer:
    • Pointer on: releases capture (only if held), unlocks mouse, sets CaptureDuringMouseDown.
    • Pointer off: high-precision mouse movement, locks mouse to viewport, CapturePermanently.
  4. Resolves focus:
    • If !ShowPointer, re-applies focus every tick (keeps stealing focus back from anything that steals it).
    • If ShowPointer and the front widget (identity) hasn't changed since the last grant, lets click-to-focus take over.
    • Otherwise grants focus once to the widget's DesiredFocusWidget (or the viewport if none).

Focus arbitration

LastWidgetGrantedFocus (a TObjectKey<UWidget>) tracks the last focus target the system applied. The comparison is identity-based: if the current front widget's DesiredFocusWidget differs from LastWidgetGrantedFocus, focus is re-granted.

To force a re-grant without a widget change (e.g., after mutating DesiredFocusWidget), call RestoreFocusToFrontWidget. This sets the key to null, so the next tick's comparison will always re-grant.

Click-to-focus

When the user clicks on a focusable widget, the LuprexViewportClient notifies the player controller via ClickToFocus(Widget). On the next UpdateInputMode tick, if the system isn't going to re-grant focus for other reasons, it forwards focus to the clicked widget.

This only takes effect in pointer mode. In non-pointer mode, focus is continuously re-applied to the front widget's DesiredFocusWidget, overriding click behavior.

What NOT to do

  • Don't call AddToViewport on top-level game widgets. All top-level widgets must be children of RootCanvas. Going directly to the viewport bypasses the entire system.
  • Don't set input mode via SetInputModeGameOnly / SetInputModeUIOnly / SetInputModeGameAndUI. The window management system owns these. Adjust ShowPointer / BlockInput on the slot instead.
  • Don't call SetShowMouseCursor directly. Same reason — driven by the front widget's ShowPointer.
  • Don't set focus manually. Set DesiredFocusWidget on the widget; the system will pick it up.

Design notes

Why slot fields, not separate requests? Earlier versions of this system kept a parallel FlxInputModeRequests array of per-widget state. Tying that to slot lifecycle (widget added = entry exists; widget removed = entry gone) eliminated a whole class of stale-entry and lifecycle bugs.

Why ZOrder as the priority key? The alternative is a separate priority field, which can diverge from visual order. Using ZOrder means "the widget you see on top is the one with input priority" is true by construction, not by convention.

Why identity-based focus invalidation? Tracking a sequence number worked but required callers to call an explicit re-request API whenever DesiredFocusWidget changed. Using TObjectKey<UWidget> lets the system notice any change in the intended focus target automatically.

Why ShowPointer and not a whole EInputMode enum? Two flags cover the behaviors that actually matter (ShowPointer, BlockInput). A three-state enum with GameOnly / UIOnly / GameAndUI would force callers into categories that don't map cleanly to how this system layers widgets.