Input bindings¶
Every input-consuming feature in Tick UI reads its actions
through a per-feature bindings config. Confirming a focused
button, copying selected text, opening the debug console, and
toggling the debugger overlay are all named actions
(Confirm, Copy, Toggle) with a configurable input behind
them. This page explains the framework.
For the procedure that exercises the framework, refer to Configure input bindings.
Per-feature, polymorphic, in a container¶
Three ideas combine.
Per feature. Each input-consuming feature owns its own
bindings class. Navigation has NavigationBindings. The textbox
has TextboxBindings. The console has ConsoleBindings. The
debugger has DebuggerBindings. Adding a future feature means
adding a new bindings class without touching the others.
Polymorphic per backend. Each bindings class is abstract. Two
concrete subclasses read input natively: a legacy subclass
(LegacyXxxBindings) that uses UnityEngine.Input and an Input
System subclass (InputSystemXxxBindings) that uses
UnityEngine.InputSystem. The two backends do not share a key
type and need no translation map. Compile-time selection through
ENABLE_INPUT_SYSTEM decides which subclass XxxBindings.Default
returns for the active backend.
In a container. InputBindings is a struct with one field per
feature:
public struct InputBindings
{
public NavigationBindings Navigation;
public TextboxBindings Textbox;
public ConsoleBindings Console;
public DebuggerBindings Debugger;
}
The container sits on Configuration.Bindings and is surfaced as
UI.Bindings. Widgets resolve actions through
ui.Bindings.Feature.Action(). A null field is filled with the
per-feature default at ingest, so partial customisation works
without listing every untouched feature.
What each feature exposes¶
The action queries each feature owns:
| Feature | Actions |
|---|---|
NavigationBindings |
NavigateUp, NavigateDown, NavigateLeft, NavigateRight, Confirm, Cancel |
TextboxBindings |
SelectAll, Copy, Paste, Cut, Submit, DeleteBack, DeleteForward, CaretLeft, CaretRight, CaretHome, CaretEnd |
ConsoleBindings |
Toggle, Submit, HistoryPrev, HistoryNext, AutocompleteNext, AutocompletePrev |
DebuggerBindings |
Toggle, LockClickThroughHeld, InspectElementHeld |
Every action returns bool. Pressed-this-frame is the default.
The two held-state actions are
DebuggerBindings.LockClickThroughHeld and
DebuggerBindings.InspectElementHeld, named with the Held
suffix for clarity. InspectElementHeld is virtual rather than
abstract and returns false by default, so an existing custom
DebuggerBindings subclass keeps compiling; opt in by overriding
it.
How a binding describes a key¶
Two helper structs unify per-backend key descriptions:
LegacyKeyBinding: aUnityEngine.KeyCodeand aModifierMask. AnyKeyCodeis bindable, includingJoystickButton0throughJoystickButton19for controller buttons. An implicit conversion fromKeyCodekeeps no-modifier bindings terse.InputSystemKeyBinding: aUnityEngine.InputSystem.Keyand aModifierMask, with an implicit conversion fromKey. These keyboard-shortcut features (textbox, console, debugger) bind keyboard keys only; the gamepad-awareInputSystemControltype (Key?orGamepadButton?, with implicit conversions) is used byInputSystemNavigationBindings.
Each binding helper exposes static AnyPressed(List<...>),
AnyHeld(List<...>), and AnyHeldKey(List<...>) helpers. Each
concrete bindings subclass stores a List per action and forwards
the action method to one of those helpers.
NavigationBindings is the exception. It pre-dates the helper
structs and keeps a plain List<KeyCode> (legacy) or
List<InputSystemControl> (Input System) for each direction, plus
a separate ConfirmKeys / CancelKeys (legacy) or
ConfirmControls / CancelControls (Input System). Navigation
does not support modifier shortcuts because none of its actions
need them.
Modifier matching¶
ModifierMask is a [Flags] enum: None, Ctrl, Shift,
Alt. A binding's modifier mask matches the held modifier
state exactly. Ctrl+Shift+C does not trigger a
binding registered as Ctrl+C. This is what lets Tab and
Shift+Tab bind separately to AutocompleteNext and
AutocompletePrev in the console.
To accept multiple modifier combinations for the same action, register the action with multiple binding entries (one for each combination).
The per-frame update¶
InputBindings.Update runs once per frame from inside
BeginFrame, after the raw input pump. Each feature's Update
chains from there. Most features have nothing to do here. The
legacy navigation backend uses its Update to edge-detect the
D-pad axes, because legacy Input has no built-in D-pad button
(refer to Configure input bindings
for the project setup it requires).
What lives outside bindings¶
IInput is the raw-input surface that providers expose to the
UI. Bindings own all named actions. IInput keeps only what
bindings cannot replace:
- Mouse position, delta, scroll delta.
- Mouse buttons: down, up, held, and left-double-click.
- Modifier-held queries:
GetCtrlHeld,GetShiftHeld,GetAltHeld. The textbox uses these to gate typed-character insertion (do not insertcwhileCtrl+Cis being pressed). The debugger uses them for non-bindable modal state, such as scrub speed inside the layout preview. PeekTextInput/ConsumeTextInput: the raw typed-character stream the textbox inserts into its buffer.PeekTextInputreturns aReadOnlySpan<char>view (no per-frame string allocation); the textbox callsConsumeTextInputonce it has read the characters so they are not delivered again.
A custom IInput implementation supplies these uninterpreted
inputs. Bindings consume input through the providers' static APIs
(Keyboard.current, Gamepad.current, Input.GetKey), not
through the IInput surface.
Configuration entry points¶
Two entry points cover setup and runtime:
Configuration.Bindings: pass anInputBindingsvalue to theUIconstructor through theConfiguration. Null fields are filled with the per-featureDefaultbefore the first frame runs.UI.SetBindings(InputBindings): replace bindings at runtime. Useful for an in-game keybinding screen that mutates bindings live.
For the procedure that exercises both entry points, refer to Configure input bindings.
Pitfalls¶
A shortcut fires for Ctrl+Shift+C but not for Ctrl+C alone.
: Modifier match is exact. Add a separate binding entry to
the same action for each modifier combination you want to
accept.
The legacy D-pad does not move focus.
: The default axis names (DPadHorizontal, DPadVertical)
are not present in the project's Input Manager. Add them
under Project Settings > Input Manager mapped to the
controller D-pad, rename them on
LegacyNavigationBindings.DpadHorizontalAxis /
DpadVerticalAxis, or set UseDpad to false.
Customising one feature appears to reset the others.
: Each LegacyXxxBindings constructor starts with empty
Lists for every action. Customisation built up field-by-field
on a fresh instance loses the defaults for every field you do
not touch. Start from the per-feature Default, mutate the
fields you care about, then assign the result to
InputBindings.
The debugger's lock-click-through never fires.
: LockClickThroughHeld defaults to "either Ctrl key held"
and uses AnyHeldKey, which ignores the modifier-mask
comparison. The binding key is itself a modifier; a custom
rebinding should keep the same helper, not switch to
AnyHeld.
Additional resources¶
- Configure input bindings: the procedure that customises bindings at setup and at runtime.
- Navigation: the consumer of
NavigationBindings. - Configuration: the configuration struct that holds bindings.