Navigation¶
Navigation routes directional input, confirm, and cancel to widgets that hold focus, without depending on the mouse. A focused button responds to confirm the same way a hovered button responds to a click. The same UI becomes usable with the keyboard, a gamepad, or any other source the bindings drive.
This page explains the model. For the procedure that applies it, refer to Build a navigable menu. For the configurable inputs that trigger navigation actions, refer to Input bindings.
One active block, a set of focusable elements¶
Navigation lives inside a navigation block. A block is a scope that contains navigation elements. At any moment one block is active, and one element inside the active block is focused. Directional input moves focus between elements in the active block. Confirm activates the focused element. Cancel steps back out of a block that was entered by confirming an owning element.
A block is declared with BeginNavigation / EndNavigation:
ui.BeginNavigation("main_menu", active: true, initialFocus: "new_game");
{
using (ui.Auto().Pad(20).PushSeriesVertical(32, 5))
{
if (MenuItem(ui, "new_game", "New game")) StartNewGame();
ui.Next();
if (MenuItem(ui, "load_game", "Load game")) LoadGame();
ui.Next();
if (MenuItem(ui, "exit", "Exit")) Quit();
}
}
ui.EndNavigation();
MenuItem calls ui.NavigationElement(id) to register the row as
a focusable element, draws a focused visual when
ui.IsFocused(id) returns true, and returns ui.Button(id).
Button reads Bindings.Navigation.Confirm(), so the focused row
activates on the frame the confirm input fires.
How focus moves¶
A block stores the layout-space centre of every element registered
inside it this frame. When the active block sees a directional
input from Bindings.Navigation (up, down, left, or right), it
considers every other element in the block as a candidate. For each
one it computes the normalised direction from the focused element's
centre and takes the dot product against the input direction.
Candidates whose dot product falls below 0.5 — outside a
ninety-degree cone around the input direction — are discarded. Among
the survivors, the candidate with the highest score wins focus,
where the score is the dot product minus a small distance penalty,
so a closer element edges out a more perfectly-aligned but distant
one.
The 0.5 cone keeps a directly-down neighbour winning a down press
over an element off to the side, while still letting a slightly
diagonal layout resolve. An element sitting outside the cone for one
direction stays reachable through a different directional press.
There are no edges and no manual adjacency declarations: every
element in the block is a candidate for every press. The element
geometry rebuilds from each frame's NavigationElement calls, so
reordering, removing, or inserting an element changes which
candidate wins automatically on the next frame.
Confirm and cancel¶
Both flow through Bindings.Navigation:
- Confirm:
ButtonreadsBindings.Navigation.Confirm(). The focused button activates on the same frame the configured confirm input fires. - Cancel:
EndNavigationreadsBindings.Navigation.Cancel()when the block is the active one. The built-in handler pops only a block that was entered by confirming an owning element — aNavigationElementwhose id matches the inner block's id (refer to Block lifecycle and history) — and returns focus to that element. A block entered any other way, such as a plainPushNavigation, ignores the built-in cancel; the app readsBindings.Navigation.Cancel()itself and callsPopNavigationto drive the back step.
For the inputs that map to confirm and cancel by default, and how to change them, refer to Input bindings.
Block lifecycle and history¶
Navigation blocks form a stack. Calling PushNavigation(id)
deactivates the current active block, saves its focused element,
and activates the target block on the next frame. Calling
PopNavigation restores the previous block and the focus it had
when it was last active.
A typical pattern is a settings submenu that pushes itself when
the player chooses it. To pop it back on cancel, read
Bindings.Navigation.Cancel() inside the submenu and call
PopNavigation — a PushNavigation'd block has no owning element,
so the built-in cancel handler leaves it in place:
if (MenuItem(ui, "settings", "Settings"))
ui.PushNavigation("settings_menu");
ui.BeginNavigation("settings_menu", initialFocus: "volume");
{
// settings items
if (ui.IsNavigationActive("settings_menu") && ui.Bindings.Navigation.Cancel())
ui.PopNavigation();
}
ui.EndNavigation();
The state of a block reflects in three queries:
IsActiveNavigationBlock(id): the named block is the innermost currently-openBeginNavigationscope (the top of the render stack).IsNavigationActive(id): the given block is at the top of the navigation history.IsNavigationFirstFrame(id): the block became active this frame.
What blocks the input affects¶
Directional inputs, confirm, and cancel apply only to the active
block. Other blocks rendered the same frame keep their last
focused element but do not respond to navigation actions.
Button calls inside an inactive block still respond to the
mouse path; they ignore confirm.
Pitfalls¶
A navigation element registers in the wrong block.
: NavigationElement registers in the block at the top of the
navigation stack at the time of the call. Calling it inside a
nested BeginNavigation scope routes it to the nested block.
Place BeginNavigation carefully around the elements that
belong to it.
Directional input never moves between two elements. : Neither element falls inside the other's ninety-degree cone for the press, or both register in different blocks. Confirm the two elements register in the same block, and lay them closer to a cardinal direction relative to each other.
A diagonal between two elements never wins focus.
: The 0.5 dot-product cone excludes elements more than ninety
degrees off the input direction. Lay the focused widgets out
closer to a cardinal direction relative to each other so the
target falls inside the cone.
The built-in cancel does nothing.
: The built-in handler pops a block only when it is the active one
and it was entered by confirming an owning element. A block
that is rendered but not active ignores cancel; so does a block
entered by a plain PushNavigation. For the latter, read
Bindings.Navigation.Cancel() in the app and call
PopNavigation to step back.
Additional resources¶
- Build a navigable menu: the procedure that applies this model.
- Input bindings: the configuration layer that decides which physical inputs trigger navigation actions.
- Input and focus: the separate model that resolves which panel owns the mouse.