Input
Use mouse, keyboard, or gamepad with the same CalmUI screen.
Who owns the input mode
By default CalmUI applies the player controller's input mode itself on every
screen input-policy change: FInputModeGameOnly under Passthrough,
FInputModeGameAndUI otherwise, plus the move/look ignore flags; the
screen's mouse-cursor flag applies either way. GameOnly puts the viewport
into permanent mouse capture, so a host that owns its own input contexts
(Enhanced Input) must turn this off and drive modes itself from
OnCalmUiInputPolicyChanged, or the click that should re-acquire capture
after a screen closes gets swallowed before it reaches PlayerInput.
Set Project Settings → Plugins → CalmUI → Apply host input mode to off, or
add this to DefaultGame.ini:
[/Script/CalmUiRuntime.CalmUiProjectSettings]
bApplyHostInputMode=FalseWith it off, bind OnCalmUiInputPolicyChanged on UCalmUiLocalPlayerSubsystem
and drive your own contexts, capture and lock rules from it, reading
IsCalmUiBlockingGameInput(), IsCalmUiPointerOverUi() and
GetCalmUiInputPolicy() as needed. An overlay panel shown under a
game-and-ui/passthrough policy counts as UI for the pointer-over check only
when its root element is marked data-calmui-interactive="true".
See Input profiles and rebinding for the full contract, including why leaving both the host and CalmUI driving input modes makes them fight each other.
CalmUI supports:
- Mouse move / left button
- Arrow keys, D-pad and left-stick navigation
- Tab / Shift+Tab focus
- Enter, Space, bottom face button = Accept
- Escape, right face button = Back
- Left face button (X / □) = Secondary, top face button (Y / △) = Tertiary (no keyboard default)
- Mouse-wheel and input-profile Scroll event delivery
- Right and middle mouse buttons
- Hold to activate (
ui:hold-ms) for the primary button, Enter/Space and the face button
Focus moves between available controls. Hidden and disabled controls are skipped. Use Studio's preview to check the basic interaction, then test the final input inside Unreal.
Input is owned by the player's UCalmUiLocalPlayerSubsystem. Two Local Players
have separate queues, focus, active-input state, and persisted bindings. The
top blocking Modal remains the only interactive View until it closes; input also
continues to be serviced while gameplay is paused.
See Input profiles and rebinding to configure Enhanced Input, persistence, and the C++/Blueprint API.
Left, right, and middle mouse buttons all reach CalmUI: the default input profile
binds PointerPrimary to the left button, PointerSecondary to the right
button, and PointerMiddle to the middle button (ECalmUiInputAction in
CalmUiInputProfile.h). Every button dispatches PointerDown/PointerUp to
Core's InteractionRuntime, which synthesizes a click event with a
DOM-numbered button field (0 primary, 1 middle, 2 secondary) for any button;
only a primary press can start a drag interaction, so a secondary or middle
press over a draggable control clicks instead of dragging. Pointer-over-UI
consumption is button-agnostic: whichever button is pressed, a hit over CalmUI
is consumed the same way a primary click is, and a press over the game world is
left to the host exactly as it already is for the left button.
Hold to activate
An element authored with ui:hold-ms="2000" (see
Hold to activate) needs its
activation input held: the primary pointer button pressed on it, or Accept
pressed while it is focused. The subsystem already delivers PointerDown/
PointerUp for the primary button; for Accept it now also delivers the
release of every bound Accept key (ECalmUiHostInputType::AcceptRelease,
from both the Slate navigation pre-processor's key-up and the controller
polling fallback), which is what cancels a hold that ends early. The hold itself
advances on the shared UI clock (CalmUiClock::UiDeltaSeconds(), the same
clamped delta transitions and keyframes use) inside the scene view extension, so
a hitch cannot complete a hold early, and on completion the element's normal
click/accept is dispatched once with hold: true. During the hold the
element carries :holding and publishes --calmui-hold-progress to itself and
its descendants; the renderer re-resolves only that subtree as the value changes.
A plain press/release on such an element does nothing.
World surfaces expose the same pair through Blueprint:
AcceptCalmUiWorldSurface starts the hold and the new
ReleaseAcceptCalmUiWorldSurface cancels it. Fixtures drive it with
pointer_down / accept, advance_time, pointer_up / accept_release, and
assert assert_state ... "state": "holding" and
assert_style ... "property": "hold-progress" (R4HoldToActivate).
Secondary and tertiary
ECalmUiInputAction::Secondary and ECalmUiInputAction::Tertiary are the two
extra activations of the focused control that a gamepad offers beyond Accept
and Back (see Secondary and tertiary
activations).
The built-in profile binds them to Gamepad_FaceButton_Left (X / □) and
Gamepad_FaceButton_Top (Y / △) and to no keyboard key; a press enqueues
ECalmUiHostInputType::Secondary / Tertiary and the scene view extension
calls InteractionRuntime::Secondary() / Tertiary(), which dispatches one
secondary / tertiary event to the focused control, bubbling like Accept.
There is no release counterpart because there is no hold variant.
Delivery follows Accept exactly: the Slate navigation pre-processor routes the
key down (OS key repeats are ignored) while the game viewport has focus and a
screen owns input; the controller polling fallback uses the same
"just pressed" edge and the same guards, so a passthrough screen leaves the
buttons to the game and a screen pushed or popped this frame does not receive
the press that changed the stack. The text sink keeps ownership of a keyboard
chord while a field is being edited.
To give the keyboard a chord, add a binding for the action in your
UCalmUiInputProfile asset (for example Enter with Shift for Secondary
and Enter with Alt for Tertiary, which is what Studio's preview maps)
or rebind at run time with Rebind CalmUI Input: the modifier flags on
FCalmUiInputBinding make Shift+Enter a different chord from the plain
Enter that Accept uses, so the profile still validates. A player's
persisted overrides that predate the two actions receive the profile defaults
for them on load unless the default chord is already taken by a saved
binding. Get CalmUI Action Prompt returns the bound key for both actions,
so an ActionPrompt can show "□ Drop" / "△ Compare" next to "✕ Use".
World surfaces expose SecondaryCalmUiWorldSurface and
TertiaryCalmUiWorldSurface, the counterparts of AcceptCalmUiWorldSurface.
Fixtures drive the actions with the secondary / tertiary steps and assert
them with assert_event ... "event": "secondary" / "tertiary" and
assert_component_event (now also with exactCount). See
R4SecondaryActivation.
Wheel input scrolls overflow: auto and overflow: scroll containers. Editable
controls use the shared Core editor, with host keyboard and clipboard delivery.
See Scrolling and text entry for
the GameUI examples and platform input-method validation boundaries.
Pointer drag/drop can cross simultaneously interactive screen Views in the same
player viewport when the source drag group matches the destination drop group.
The frontmost hit View owns the destination; an incompatible foreground panel
blocks targets behind it. The drag ghost is painted after all screen layers.
dragenter and drop carry { key, target, accepted }: key is the source drag
key, target is the destination drop key, and accepted reports Core group
compatibility, not gameplay authorization. Game handlers still validate transfers.
dragleave clears destination feedback on departure, drop, or cancellation.
Hiding a participating View or changing the dragged source identity cancels the
operation. Keyboard/gamepad pickup remains within its current View. Studio's
single-View WASM preview uses the same pointer coordinator and event payloads.