simvx.core.decorators

Lifecycle hook decorators for Node subclasses.

A node’s per-frame, lifecycle, and input behaviour is defined by methods named on_ready, on_update, on_fixed_update, on_input, on_enter_tree, on_exit_tree, on_draw, on_gui_input and on_picked. Subclasses override these directly to define one “primary” handler per hook.

The decorators in this module mark additional methods (with arbitrary names) as extra handlers for the same hooks, allowing concerns to be split across methods without forcing one monolithic override::

class Player(Node):
    @on_update
    def update_animation(self, dt):     # additional handler (fires after the override)
        self._frame += dt * self.fps

    def on_update(self, dt):           # primary override (always fires first)
        self.position += self.velocity * dt

    @on_input("jump", consume=True)     # an action's press
    def jump(self, event):
        self.velocity.y = -300          # claims the event: consume=True and no refusal

    @on_input(Key.CTRL + Key.S)         # a key, with a modifier held
    def save(self, event):
        self.tree.save_game()

A decorated handler is declared above a same-named override, or its decorator is qualified (@decorators.on_update): in a class body a name defined earlier wins, so @on_update below def on_update would call the override rather than the decorator. tools/decorator_shadowing.py finds the mistake.

The decorators are bare-or-called: @on_update and @on_update() are both valid. @on_input(...) requires the call form when filters are passed; bare @on_input is the all-events catch-all. @on_gui_input and @on_picked take the same filters for the targeted phase, the input aimed at one node, and are run by that node rather than broadcast.

All hook collection happens once in :meth:Node.__init_subclass__: runtime dispatch is a tuple iteration, never a name lookup.

Module Contents

Classes

ButtonEdge

Which edge of a press an @on_input button or action filter hears.

TargetedHandlers

One class’s @on_gui_input or @on_picked handlers, indexed by the input each routes.

Functions

on_input

Mark a method as an input event handler with optional filters.

on_gui_input

Mark a method as a handler for input aimed at this node in particular.

on_picked

Mark a method as a handler for a 3D pick of this node.

collect_targeted

Index the @on_gui_input and @on_picked handlers cls and its bases declare.

collect_hooks

Collect lifecycle and input handlers declared on cls and its bases.

Data

API

simvx.core.decorators.on_ready

‘_make_simple(…)’

simvx.core.decorators.on_update

‘_make_simple(…)’

simvx.core.decorators.on_fixed_update

‘_make_simple(…)’

simvx.core.decorators.on_enter_tree

‘_make_simple(…)’

simvx.core.decorators.on_exit_tree

‘_make_simple(…)’

simvx.core.decorators.on_draw

‘_make_simple(…)’

class simvx.core.decorators.ButtonEdge[source]

Bases: enum.StrEnum

Which edge of a press an @on_input button or action filter hears.

A handler named after a button or an action hears it go down and nothing else, as every other library’s event does: @on_input("shoot") shoots once per press. Asking for the release, or both, is a deliberate choice.

Initialization

Initialize self. See help(type(self)) for accurate signature.

PRESS

‘press’

RELEASE

‘release’

BOTH

‘both’

__new__(*values)
__add__()
__contains__()
__delattr__()
__dir__()
__eq__()
__format__()
__ge__()
__getattribute__()
__getitem__()
__getnewargs__()
__getstate__()
__gt__()
__hash__()
__iter__()
__le__()
__len__()
__lt__()
__mod__()
__mul__()
__ne__()
__reduce__()
__reduce_ex__()
__repr__()
__rmod__()
__rmul__()
__setattr__()
__sizeof__()
__str__()
__subclasshook__()
capitalize()
casefold()
center()
count()
encode()
endswith()
expandtabs()
find()
format()
format_map()
index()
isalnum()
isalpha()
isascii()
isdecimal()
isdigit()
isidentifier()
islower()
isnumeric()
isprintable()
isspace()
istitle()
isupper()
join()
ljust()
lower()
lstrip()
partition()
removeprefix()
removesuffix()
replace()
rfind()
rindex()
rjust()
rpartition()
rsplit()
rstrip()
split()
splitlines()
startswith()
strip()
swapcase()
title()
translate()
upper()
zfill()
__deepcopy__(memo)
__copy__()
name()
value()
simvx.core.decorators.on_input(*args: Any, **kwargs: Any) → Any[source]

Mark a method as an input event handler with optional filters.

Filters are routing keys, so every one of them is hashable and static: the dispatch tables are indexed by input and only the handlers registered for an event are ever invoked. There are deliberately no predicates, which are a body if moved into the decorator and cannot route.

The target is positional, and its type says what it is::

@on_input("jump")                            # an action opening
@on_input(Key.CTRL + Key.S)                  # a key, with a modifier held
@on_input(Pointer.PRIMARY)                   # any pointer's main press
@on_input(Controller.A, edge="release")         # a pad button coming up
@on_input(Controller.LEFT_STICK)                # every motion of the stick
@on_input(Touch.PINCH)                        # a gesture is a control too
@on_input(PointerEvent, phase=PointerPhase.MOVE)   # an event family
@on_input                                     # every event

A tuple is any-of, within one kind: @on_input((Key.A, Key.LEFT)).

Edge: edge: :class:ButtonEdge or its string. An action or a button target hears the press by default ("press"); "release" hears it come up and "both" hears both. A pointer button’s press is its DOWN and its release its UP; a pointer’s MOVE, ENTER, LEAVE and CANCEL are the PointerEvent family’s, asked with phase=: one phase, a tuple of them, or None for every phase, as @on_input(PointerEvent) hears.

Repeat: repeat: True on a key target also hears the OS auto-repeats of the held key, each a further press with event.echo set: a cursor that walks a grid while an arrow is held. The default hears one press per physical press. A repeat never opens an action and reaches no event family or bare handler in this phase, so the option belongs to a key target alone. In the capture phase an event family hears the repeats it always has. In either phase a key’s repeats belong to the node that claimed its press: only that node’s handlers hear them, and in the capture phase they are claimed for it whether or not it asked to hear them.

Held controls: A button target can hold other controls, written with + as a binding is: Key.CTRL + Key.S, Key.CTRL + Pointer.PRIMARY, Controller.LEFT_BUMPER + Controller.A. It is read by the same rule as a binding (:class:~simvx.core.input.events.InputBinding): a target holding a modifier is exact, so Ctrl+S is not Ctrl+Shift+S, and one holding none ignores modifiers. exact: Overrides that default for every button target: True on Key.S hears S only with no modifier down, False on Key.CTRL + Key.S hears it under Shift too. An action target refuses it, because the action’s bindings already say.

Claim: consume: True claims the event whenever the handler runs, which stops every later handler in the broadcast. The default is False: composition in a tree is additive, and a handler that claims by default subtracts invisibly.

Phase: capture: True hears the event in the capture phase, root-first and ahead of every control, before the focused text field or any game handler: how an editor keeps F5 and Ctrl+S whatever has focus. A claim there is the whole event, its release included. Device inputs only; an action has not resolved yet.

Motion: samples: True on the PointerEvent family hears every motion sample as it arrives, each its own event with its own position, pressure, tilt and time, rather than the one merged move a frame that every other handler hears: what a drawing canvas under a pen wants. Such a handler hears no merged move, and its presses and releases arrive as usual. A sample reaches only these handlers, which claim among themselves by the usual return.

A handler returns True to claim this call, False to decline it, or None (including by falling off the end) to defer to consume=. Anything else raises, naming the handler. A claim changes no device state: Input.is_key_pressed and the rest stay honest whoever claimed what.

simvx.core.decorators.on_gui_input(*args: Any, **kwargs: Any) → Any[source]

Mark a method as a handler for input aimed at this node in particular.

The decorator form of :meth:Node.on_gui_input, for the targeted phase: keys and text reach the focus owner, a pointer reaches the node holding it or the controls under it, and the wheel goes along that chain. The filter is @on_input’s grammar, routed the same way, so only the handlers registered for an event are invoked::

@on_gui_input(ScrollEvent)                        # the wheel
@on_gui_input((PointerEvent, ScrollEvent))        # either family
@on_gui_input(Pointer.PRIMARY, edge="release")    # the main button coming up
@on_gui_input(PointerEvent, phase=PointerPhase.MOVE)
@on_gui_input(Key.CTRL + Key.C)                   # a key, with a modifier held
@on_gui_input                                     # every event this phase delivers

A bare @on_gui_input hears all four event classes the phase delivers:

Class:

PointerEvent, :class:ScrollEvent, :class:KeyEvent and

Class:

TextEvent. edge=, phase=, exact=, repeat= and consume= mean what they mean on @on_input: a key target hears one press per physical press, and repeat=True adds the OS auto-repeats of the held key, each with event.echo set, as a key that walks a list wants. An event family and a bare @on_gui_input hear every event the phase delivers, repeats included, as the :meth:Node.on_gui_input override does, which is the feed a text field reads. A key’s repeats and its release go to the control that claimed its press, so a release handler hears its key only after a press handler on the same node claimed (consume=True, or edge="both" returning True on the press), and a repeat its node declines is still that node’s: it never falls through to focus traversal or the game. What never reaches a control is refused when the class is defined: an action name (actions resolve after this phase, from what no control claimed), a pad’s button or axis, and capture= and samples=, which are the broadcast’s.

The handler receives the event and claims it by the rule every input handler answers to: True claims, False declines, and None defers to consume=. A claim stops the event at this node, exactly as one from :meth:Node.on_gui_input does.

A class may declare several. For one event they run in declaration order, base classes first, until one claims; the :meth:Node.on_gui_input override, the catch-all, runs after them, and only if none claimed. The pointer bubble (press, release, wheel) skips a node whose class declares neither; keys and text reach the focus owner, and motion and a captor’s events reach their target, whatever its class declares.

simvx.core.decorators.on_picked(*args: Any, **kwargs: Any) → Any[source]

Mark a method as a handler for a 3D pick of this node.

The decorator form of :meth:Node.on_picked. A bare @on_picked hears every press’s pick; a pointer button narrows it to the picks cast for that button, matched as @on_input matches a press, so a role hears every pointer::

@on_picked(Pointer.PRIMARY)                   # any pointer's main press
@on_picked(Mouse.RIGHT)                       # the context-menu click
@on_picked((Mouse.MIDDLE, Pen.BARREL_1))      # any of them
@on_picked(Key.SHIFT + Pointer.PRIMARY)       # with Shift held, and nothing else
@on_picked(Pointer.PRIMARY, edge="release")   # the button coming up over this node
@on_picked                                    # every press's pick

edge=, exact= and consume= mean what they mean on @on_input. A button’s pick hears its press by default; "release" hears the pick cast where the button comes up, and "both" either, told apart by event.pressed. A release casts a ray only while a node in the tree declares such a handler, and only those handlers hear it: a bare @on_picked and the :meth:Node.on_picked override hear presses alone. A release belongs to whoever claimed its press, so when a node claimed the press, a release pick’s claim stops only this node’s later handlers. When nobody claimed the press, a release pick that claims stops every later handler, as any claim does, so no broadcast or action handler hears the release; the action the press opened still closes, and just_released and the physics tick report it. phase= is refused, as is anything that is not a pointer button.

The handler receives the :class:~simvx.core.input.events.PickEvent and claims it by the rule every input handler answers to: True claims, False declines, and None defers to consume=. For one pick the decorated handlers run in declaration order, base classes first, until one claims; the :meth:Node.on_picked override runs after them, and only if none claimed.

class simvx.core.decorators.TargetedHandlers(handlers: collections.abc.Iterable[tuple[str, simvx.core.decorators._InputFilter]])[source]

One class’s @on_gui_input or @on_picked handlers, indexed by the input each routes.

Built once per class at definition, so an event looks up only the handlers registered for it, under the same routing keys the broadcast tables use: a key and its edge, a key’s auto-repeat, a pointer button (in any of the forms a press is named by) and its phase, a phase alone, an event class, and the catch-all. A pick is looked up as a pointer press or release. picks_release says whether any handler hears a release pick, which is what lets the router skip casting a ray for a release nobody asked for.

Initialization

__slots__

(‘keys’, ‘repeats’, ‘pointer’, ‘classes’, ‘catch_all’, ‘picks_release’)

matching(event: simvx.core.input.events.InputEvent, button_forms: collections.abc.Callable[[simvx.core.input.events.InputEvent], collections.abc.Iterable[simvx.core.input.controls.DeviceButton]]) → list[tuple[simvx.core.decorators._TargetedEntry, simvx.core.input.events.InputEvent]][source]

The entries event reaches, each with the event as it is delivered, in declaration order.

button_forms names a pointer press in every form a filter can name it by; it is asked only when a pointer-button filter exists. A handler reached under several keys appears once per key, so the dispatch can test each one’s chord and run the handler under the first that holds. A key’s auto-repeat reaches only the key targets that asked for it (repeat=True), and the families and catch-alls. A pick hears its press as the DOWN phase and a release cast as UP, so a button filter hears the release only when its edge asks, and a release pick reaches no catch-all.

simvx.core.decorators.collect_targeted(cls: type) → tuple[simvx.core.decorators.TargetedHandlers | None, simvx.core.decorators.TargetedHandlers | None][source]

Index the @on_gui_input and @on_picked handlers cls and its bases declare.

Declaration order across the MRO, base classes first, as

Func:

collect_hooks gathers the rest. None for a hook the class declares no decorated handler for, which is what keeps a node without one off the targeted path’s extra work entirely.

simvx.core.decorators.collect_hooks(cls: type, primary_methods: collections.abc.Iterable[str]) → tuple[dict[str, tuple[str, ...]], tuple[tuple[str, dict[str, Any]], ...]][source]

Collect lifecycle and input handlers declared on cls and its bases.

Walks the MRO most-derived-last (matching Property.__set_name__):

  • Decorated methods (@on_update etc.) are gathered in declaration order across the MRO.

  • Same-named primary overrides (def on_update(self, dt):) are treated as implicit primary handlers and pinned to index 0 of their bucket: users do not need to apply @on_update to their own on_update override for it to fire.

  • An on_input override declaring no filter is the catch-all, exactly as a bare @on_input is. Input handlers are routed by filter rather than by bucket order, so it has no index to be pinned to.

Returns (hooks, input_handlers): hooks: dict mapping hook name (e.g. "update") to an ordered tuple of method names. The primary override, if present, is always first. input_handlers: tuple of (method_name, filter_dict) pairs in declaration order across the MRO.

simvx.core.decorators.__all__

[‘on_ready’, ‘on_update’, ‘on_fixed_update’, ‘on_input’, ‘ButtonEdge’, ‘on_enter_tree’, ‘on_exit_tre…