simvx.core.ui.focus

The focus model: one authority for who can hold focus and who is in the tab order.

focus_mode is that authority, on any node, on two independent axes:

  • Can it hold focus? focus_mode.takes_focus (anything but NONE) – :func:is_focusable.

  • Is it in the tab order? focus_mode.in_tab_order (ALL or HOVER) – :func:is_tab_stop.

Both axes additionally require the node to be enabled and effectively visible (through the whole ancestor chain, visible_in_tree): a node the player cannot see or use is not one the keyboard can reach. A CLICK node can therefore hold focus without ever being a Tab destination, which is the web platform’s tabindex="-1".

The search is over the tree’s index of nodes whose focus_mode takes focus, kept as they enter, leave and change mode, so Tab and a D-pad never walk the sprites and the particles between the buttons.

func:

next_focus is the single traversal routine. Everything that moves focus by keyboard goes through it: the UI router’s Tab handling, Control.focus_next_control and its reverse. It walks the scope in pre-order and resolves the successor from the focus owner’s POSITION in that walk, so an owner that is not itself a tab stop (a clicked Button) still hands over to its neighbour rather than restarting at the first control.

Module Contents

Functions

is_focusable

True when node may hold keyboard focus: visible, enabled, and a focus_mode that takes it.

is_tab_stop

True when node is a Tab destination: visible, enabled, focus_mode ALL or HOVER.

tab_stops

The Tab order inside scope_root: its tab stops in pre-order.

first_tab_stop

First tab stop inside scope_root in pre-order, ignoring skip.

next_focus

The control Tab (or Shift+Tab, with reverse) should focus next inside scope_root.

focus_neighbour

The control a press of direction should focus inside scope_root.

scope_root_for

The traversal scope for control: the capturing overlay chain it sits in, else the root.

Data

API

simvx.core.ui.focus.__all__

[‘first_tab_stop’, ‘focus_neighbour’, ‘is_focusable’, ‘is_tab_stop’, ‘next_focus’, ‘tab_stops’]

simvx.core.ui.focus.is_focusable(node: simvx.core.node.Node) → bool[source]

True when node may hold keyboard focus: visible, enabled, and a focus_mode that takes it.

simvx.core.ui.focus.is_tab_stop(node: simvx.core.node.Node) → bool[source]

True when node is a Tab destination: visible, enabled, focus_mode ALL or HOVER.

simvx.core.ui.focus.tab_stops(scope_root: simvx.core.node.Node) → list[simvx.core.node.Node][source]

The Tab order inside scope_root: its tab stops in pre-order.

simvx.core.ui.focus.first_tab_stop(scope_root: simvx.core.node.Node, *, skip: Any = None) → simvx.core.node.Node | None[source]

First tab stop inside scope_root in pre-order, ignoring skip.

simvx.core.ui.focus.next_focus(scope_root: simvx.core.node.Node, current: simvx.core.node.Node | None, *, reverse: bool = False) → simvx.core.node.Node | None[source]

The control Tab (or Shift+Tab, with reverse) should focus next inside scope_root.

current is the focus owner, or None. An explicit focus_next / focus_previous link on the owner wins when it points at a focusable control inside the scope; a link to a hidden, disabled or NONE-mode control falls through to the walk rather than focusing something unreachable.

Otherwise the scope is walked in pre-order and the search starts from the owner’s position in that walk, wrapping at the ends. An owner absent from the walk (a different scope, or None) starts at the first tab stop, or the last one under reverse. Returns None when the scope holds no tab stop at all.

simvx.core.ui.focus.focus_neighbour(scope_root: simvx.core.node.Node, current: simvx.core.node.Node | None, direction: simvx.core.ui.enums.FocusDirection | str) → simvx.core.node.Node | None[source]

The control a press of direction should focus inside scope_root.

An explicit focus_up / focus_down / focus_left / focus_right link on the owner wins when it points at a focusable control inside the scope; a link to a hidden, disabled or NONE-mode control falls through to the search rather than focusing something unreachable, exactly as

Func:

next_focus treats its own links.

Otherwise the search is geometric, over the same destinations Tab reaches (:func:is_tab_stop), using each control’s focus_rect. A candidate counts only if its centre lies in direction at all; among those, the nearest wins, with a sideways offset charged against it and charged much more heavily when the two rects do not overlap on the perpendicular axis, so Down prefers the control directly below to a nearer one off to the side.

With no focus owner the first tab stop is elected, which is what lets a controller player enter a menu that nothing has focused yet. Nothing in that direction returns None rather than wrapping: a wrap makes a directional press jump across the screen, which reads as a bug.

simvx.core.ui.focus.scope_root_for(control: simvx.core.node.Node) → simvx.core.node.Node[source]

The traversal scope for control: the capturing overlay chain it sits in, else the root.

Keyboard traversal never leaves an open capturing overlay, so a dialog’s Tab order is the dialog. Outside one (or for a control beneath one), the scope is the whole tree.