simvx.core.ui.ui_input¶
UIInputManager: UI input routing, focus, overlay scope, hit-testing.
All UI input flows through one contract: Node.on_gui_input(event), which
Control extends with the gui_input signal.
The mouse events that bubble – a press, a release and the wheel – are offered
to the control under the cursor and then to each of its ancestors until one
claims the event by returning True from on_gui_input; all three take that
one path (_bubble).
What is “under the cursor” is a chain rather than one control (_hit_chain):
a pointer_filter of PASS is hit-testable without blocking, so the
control behind it is a candidate too and is offered the event once the nearer
chain has declined it. STOP ends the chain and IGNORE never joins it.
That chain is the draw walk run backwards, so CanvasLayer.layer and
z_index decide what is in front for the pointer exactly as they do for the
eye: if it can be seen it can be clicked.
Mouse motion does not bubble. It reaches the node holding the pointer while a
capture is in force and otherwise the control under the cursor alone
(_handle_mouse_move), so a widget that tracks the pointer – Slider,
ColourPicker – reads motion through the same on_gui_input without an
ancestor ever seeing it.
Every finger and a pen are pointers of their own and arrive the same way the
mouse does. A control whose accepts_pointers is PRIMARY is offered the
primary pointer only, which is the mouse or whatever stands in for it; ALL
is offered every pointer, each on its own id.
Capture is per pointer and lives on Node (capture_pointer /
release_pointer), in one table keyed by pointer id: 0 is the mouse, and
every finger and pen has the opaque id the router gave it.
When a capturing overlay is open (registered via Control.show_overlay),
routing is gated to that overlay’s chain (tree.overlays): a click outside the
chain dismisses it, keyboard events go to the focused descendant with router-level
fallbacks for focus traversal and dismissal. A capture="none" overlay never
scopes input, so the world keeps full input beneath it.
Which input does what is not decided here: the router asks ui/navigation.py
whether what arrived is bound to ui_focus_next, ui_focus_prev,
ui_accept, ui_cancel or one of the four directions, so navigation is
rebindable through InputMap like any other action and any device can drive
it. A controller arrives through ui_pad and moves focus by geometry
(focus.focus_neighbour) rather than along the tab order, because a D-pad
points at the screen.
Module Contents¶
Classes¶
Routes UI input events to controls: focus, hover, pointer capture, overlay scope. |
Data¶
API¶
- simvx.core.ui.ui_input.log¶
‘getLogger(…)’
- simvx.core.ui.ui_input.NAV_REPEAT_DELAY¶
0.4
- simvx.core.ui.ui_input.NAV_REPEAT_INTERVAL¶
0.09
- class simvx.core.ui.ui_input.UIInputManager[source]¶
Routes UI input events to controls: focus, hover, pointer capture, overlay scope.
Initialization
- captor(pointer_id: int | None = None)[source]¶
The node holding pointer_id, or None. Pointer 0 is the mouse.
Inside a dispatch, the holder on the source the event came from, which is the one a widget asking mid-handler means, and with no pointer named, the pointer of that event.
- grabbed(source_id: int, pointer_id: int) bool[source]¶
Whether a node took pointer_id with
capture_pointer, rather than by claiming a finger’s press.
- capture(node, pointer_id: int | None = None) None[source]¶
Route every event from pointer_id to node until it is released.
With no pointer named, the pointer of the event being dispatched, and the mouse outside a dispatch.
- release(node, pointer_id: int | None = None) None[source]¶
Give pointer_id back, if node is what holds it.
Matched the way :meth:
_captor_formatches, so a widget that took the pointer under one spelling of “nobody said” gives it back under the other. With no pointer named, the pointer of the event being dispatched, as :meth:capture.
- notify_subtree_removed(node)[source]¶
Release focus and any pointer capture a leaving subtree holds.
Called from
Node._exit_treeso that a destroyed or reparented focused control never lingers as the focus owner and keeps receiving keyboard events, and a captor carried out of the tree stops being handed the pointer: a node the tree no longer holds receives nothing, and capture is a second delivery path that does not inherit that for free. The overlay registry’s own focus-restore path (OverlayLayer.close) handles overlay teardown separately.
- source_captures(source_id: int) list[int][source]¶
The pointers source_id may cancel: its own, and the unattributed.
What a router asks before cancelling. It speaks for one window, and another window’s pointer is not its to take back. Source 0 is not a window, it is “nobody said”, which is what a hand-built event carries; no window can claim those and none can leave them stranded either, so every source cancels them.
- cancel_capture(pointer_id: int, event) simvx.core.node.Node | None[source]¶
Take pointer_id back on event’s source, and tell whoever held it.
For input interrupted rather than finished: the window loses focus, the system claims the gesture, the finger is lifted off the edge of the glass. The node hears a cancel and not a release, so the button does not fire and the slider does not commit, but it knows to stop dragging. Silence is what left a splitter believing the divider was still under the cursor.
Returns the node that held the pointer, or
None.
- audit_owners() None[source]¶
Cancel every press whose owner can no longer hold it: once a frame, and free with none held.
An owner that hides, is disabled, stops hearing (paused, processing switched off) or has raised is told its press was cancelled, a
PointerPhase.CANCELfor a pointer and a release markedcancelledfor a key, and loses it: the rest of the gesture reaches nobody, and resuming does not pick it up again. An owner a script error disabled is not told, because it will never run again.
- release_grabs() None[source]¶
Drop every pointer capture without telling the nodes anything.
Teardown, where there is nobody left to tell. A capture taken away while the scene is still running goes through :meth:
cancel_captureinstead, so the node holding it hears that the gesture is over.
- ui_key(root: simvx.core.node.Node | None, event: simvx.core.input.events.KeyEvent) simvx.core.node.Node | None[source]¶
Route a :class:
~simvx.core.input.events.KeyEventto the UI.The UI receives the object the tree does, so a widget reads the physical key, its label and the four modifier flags off the same event a broadcast handler sees.
Returns the node that took the key, or
Nonewhen the UI let it through.
- ui_text(root: simvx.core.node.Node | None, event: simvx.core.input.events.TextEvent) simvx.core.node.Node | None[source]¶
Route a :class:
~simvx.core.input.events.TextEventto the UI.Text is what the player meant to type, after the layout, the dead keys and the IME have had their say, so a text field inserts
event.textand never assembles it from key presses.Returns the node that took the text, or
None.
- ui_pointer(root: simvx.core.node.Node | None, event: simvx.core.input.events.PointerEvent) simvx.core.node.Node | None[source]¶
Route a :class:
~simvx.core.input.events.PointerEventto the UI.The UI receives the object the tree does, so a widget reads the phase, the button, the pressure and the pointer id off the same event a broadcast handler sees. A locked pointer has no screen position to hit test with and is left to the game.
Returns the node that took the pointer, or
Nonewhen the UI let it through. The targeted phase casts its pick ray only on theNone, so a click on a button no longer also picks the world behind it.A finger, a pen and the mouse all take this one path. A control is offered a secondary pointer only when its
accepts_pointersisALL. A cancel reaches whoever holds the pointer and nobody else, because it ends a gesture rather than landing anywhere. A finger hovers from its press until its lift, and a pen stops hovering when it leaves the digitiser’s range.
- ui_scroll(root: simvx.core.node.Node | None, event) simvx.core.node.Node | None[source]¶
Route a :class:
~simvx.core.input.events.ScrollEventto the UI.The wheel arrives as the event the tree receives rather than as a key named after it, so a control reads the position it turned over and how far it turned, and a trackpad’s fractional delta survives the trip.
Returns the control that took the notch, or
None.
- ui_pad(root: simvx.core.node.Node | None, event) simvx.core.node.Node | None[source]¶
Route a controller button or axis to UI navigation.
This is the whole of “a controller can drive a menu”. It resolves through the action layer rather than by comparing button codes, so a game that rebinds
ui_acceptmoves the pad binding with it, and a pad binding a game adds to one of these actions works without the UI knowing anything about that pad.A press acts once; a held direction repeats through
- Meth:
flush_nav_repeat. Activation and dismissal never repeat, so one press of the bottom face button confirms one dialog however long it is held.
Returns the node the press moved to, activated or dismissed, and
Nonewhen navigation found nothing to do with it. Navigation that acted has spent the button: the game action bound to the same face button does not also fire, which is what keeps the bottom button from confirming a dialog and jumping at once.
Repeat a held direction, once per frame, while it is still held.
Called from the tree’s tick. The clock is :func:
time.monotonicand not the scene clock, because a pause menu is exactly where a player holds a direction and the scene clock has stopped.
- flush_hover(root: simvx.core.node.Node | None) None[source]¶
Recompute
mouse_overonce, from where the pointer ended up.The frame’s motion samples set a position and a flag; this spends them. Hover is a cached query rather than an event, so a pointer that crossed four widgets in one frame leaves the one it stopped on hovered, and nothing in between ever reported itself so. The walk it costs happens once whatever the mouse rate is, and not at all in a frame the pointer did not move.
- leave_hover(root: simvx.core.node.Node | None) None[source]¶
Every pointer leaves: nothing is hovered, and the hover targets forget where they were.
- window_focus(source_id: int, focused: bool) None[source]¶
Record whether the window source_id has the operating system’s keyboard focus.
Only that window moves focus by hover: a cursor passing over an inactive window’s buttons must not steal focus inside it.
- has_pending_edit() bool[source]¶
True if the focused control holds an uncommitted edit (e.g. a TextEdit).
- commit_pending_edit() None[source]¶
Commit the focused control’s in-progress edit now, if it has one.
Idempotent (no-op when nothing is pending). Consumers call this before tearing down or replacing a focused editor so the typed value is not lost when the widget is removed (e.g. the inspector rebuilding after a rename).