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

UIInputManager

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

reset()[source]

Reset transient state (called on scene change).

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_for matches, 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_tree so 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.CANCEL for a pointer and a release marked cancelled for 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_capture instead, 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.KeyEvent to 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 None when 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.TextEvent to 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.text and 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.PointerEvent to 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 None when the UI let it through. The targeted phase casts its pick ray only on the None, 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_pointers is ALL. 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.ScrollEvent to 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_accept moves 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 None when 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.

flush_nav_repeat(root: simvx.core.node.Node | None) → None[source]

Repeat a held direction, once per frame, while it is still held.

Called from the tree’s tick. The clock is :func:time.monotonic and 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_over once, 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).