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¶
Which edge of a press an |
|
One class’s |
Functions¶
Mark a method as an input event handler with optional filters. |
|
Mark a method as a handler for input aimed at this node in particular. |
|
Mark a method as a handler for a 3D pick of this node. |
|
Index the |
|
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.StrEnumWhich edge of a press an
@on_inputbutton 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
ifmoved 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 eventA tuple is any-of, within one kind:
@on_input((Key.A, Key.LEFT)).Edge: edge: :class:
ButtonEdgeor 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 itsDOWNand its release itsUP; a pointer’sMOVE,ENTER,LEAVEandCANCELare thePointerEventfamily’s, asked withphase=: one phase, a tuple of them, orNonefor every phase, as@on_input(PointerEvent)hears.Repeat: repeat:
Trueon a key target also hears the OS auto-repeats of the held key, each a further press withevent.echoset: 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:TrueonKey.Shears S only with no modifier down,FalseonKey.CTRL + Key.Shears it under Shift too. An action target refuses it, because the action’s bindings already say.Claim: consume:
Trueclaims the event whenever the handler runs, which stops every later handler in the broadcast. The default isFalse: composition in a tree is additive, and a handler that claims by default subtracts invisibly.Phase: capture:
Truehears 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:
Trueon thePointerEventfamily 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
Trueto claim this call,Falseto decline it, orNone(including by falling off the end) to defer toconsume=. Anything else raises, naming the handler. A claim changes no device state:Input.is_key_pressedand 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_inputhears all four event classes the phase delivers:- Class:
PointerEvent, :class:ScrollEvent, :class:KeyEventand- Class:
TextEvent.edge=,phase=,exact=,repeat=andconsume=mean what they mean on@on_input: a key target hears one press per physical press, andrepeat=Trueadds the OS auto-repeats of the held key, each withevent.echoset, as a key that walks a list wants. An event family and a bare@on_gui_inputhear every event the phase delivers, repeats included, as the :meth:Node.on_gui_inputoverride 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, oredge="both"returningTrueon 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, andcapture=andsamples=, which are the broadcast’s.
The handler receives the event and claims it by the rule every input handler answers to:
Trueclaims,Falsedeclines, andNonedefers toconsume=. A claim stops the event at this node, exactly as one from :meth:Node.on_gui_inputdoes.A class may declare several. For one event they run in declaration order, base classes first, until one claims; the :meth:
Node.on_gui_inputoverride, 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_pickedhears every press’s pick; a pointer button narrows it to the picks cast for that button, matched as@on_inputmatches 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=andconsume=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 byevent.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_pickedand the :meth:Node.on_pickedoverride 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, andjust_releasedand 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.PickEventand claims it by the rule every input handler answers to:Trueclaims,Falsedeclines, andNonedefers toconsume=. For one pick the decorated handlers run in declaration order, base classes first, until one claims; the :meth:Node.on_pickedoverride 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_inputor@on_pickedhandlers, 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_releasesays 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 theDOWNphase and a release cast asUP, 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_inputand@on_pickedhandlers cls and its bases declare.Declaration order across the MRO, base classes first, as
- Func:
collect_hooksgathers the rest.Nonefor 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_updateetc.) 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_updateto their ownon_updateoverride for it to fire.An
on_inputoverride declaring no filter is the catch-all, exactly as a bare@on_inputis. 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…