diff --git a/Difficult_Rocket/client/__init__.py b/Difficult_Rocket/client/__init__.py index ef8bf86..76d532a 100644 --- a/Difficult_Rocket/client/__init__.py +++ b/Difficult_Rocket/client/__init__.py @@ -179,8 +179,8 @@ class ClientWindow(Window): self.input_box = InputBox(x=50, y=30, width=300, batch=self.label_batch, text='') # 实例化 self.input_box.push_handlers(self) - self.input_box.push_handler('on_commit', self.on_input) - self.push_handlers(self.input_box) + self.input_box.set_handler('on_commit', self.on_input) + self.set_handlers(self.input_box) self.input_box.enabled = True # 设置刷新率 pyglet.clock.schedule_interval(self.draw_update, float(self.SPF)) diff --git a/libs/pyglet/event.py b/libs/pyglet/event.py index 524ed62..965db9b 100644 --- a/libs/pyglet/event.py +++ b/libs/pyglet/event.py @@ -1,38 +1,3 @@ -# ---------------------------------------------------------------------------- -# pyglet -# Copyright (c) 2006-2008 Alex Holkner -# Copyright (c) 2008-2020 pyglet contributors -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in -# the documentation and/or other materials provided with the -# distribution. -# * Neither the name of pyglet nor the names of its -# contributors may be used to endorse or promote products -# derived from this software without specific prior written -# permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# ---------------------------------------------------------------------------- - """Event dispatch framework. All objects that produce events in pyglet implement :py:class:`~pyglet.event.EventDispatcher`, @@ -44,155 +9,103 @@ Event types For each event dispatcher there is a set of events that it dispatches; these correspond with the type of event handlers you can attach. Event types are -identified by their name, for example, ''on_resize''. - -If you are creating a new class which implements -:py:class:`~pyglet.event.EventDispatcher`, or want to add new events -to an existing dispatcher, you must call `EventDispatcher.register_event_type` -for each event type: - - class MyDispatcher(pyglet.event.EventDispatcher): - # ... - - MyDispatcher.register_event_type('on_resize') +identified by their name, for example, ''on_resize''. If you are creating a +new class which implements :py:class:`~pyglet.event.EventDispatcher`, you must call +`EventDispatcher.register_event_type` for each event type. Attaching event handlers ======================== -An event handler is simply a function or method, that is called when system or -program event happens. There are several ways to add a handler for an event. +An event handler is simply a function or method. You can attach an event +handler by setting the appropriate function on the instance:: -When the dispatcher object is available as a global variable, it is convenient -to use the `event` decorator: + def on_resize(width, height): + # ... + dispatcher.on_resize = on_resize - @window.event +There is also a convenience decorator that reduces typing:: + + @dispatcher.event def on_resize(width, height): # ... -Here `window` is a variable containing an instance of `pyglet.window.Window`, -which inherits from `EventDispatcher` class. This decorator assumes that -the function is named after the event. To use the decorator with a function with -another name, pass the name of the event as the argument for the decorator: +You may prefer to subclass and override the event handlers instead:: - @window.event('on_resize') - def my_resize_handler(width, height); - # ... - -The most universal way to add an event handler is to call the `push_handlers` -method on the dispatcher object: - - window.push_handlers(on_resize) - window.push_handlers(on_resize=my_handler) - window.push_handlers(on_resize=obj.my_handler) - window.push_handlers(obj) - -This methods accepts both positional and keyword parameters. In case of keyword -arguments, the name of the event matches the name of the argument. Otherwise, -the name of the passed function or method is used as the event name. - -If an object is passed as a positional argument, all its methods that match -the names of registered events are added as handlers. For example: - - class MyDispatcher(pyglet.event.EventDispatcher): - # ... - MyDispatcher.register_event_type('on_resize') - MyDispatcher.register_event_type('on_keypress') - - class Listener(object): - def on_resize(self, w, h): - # ... - - def on_keypress(self, key): - # ... - - def other_method(self): - # ... - - dispatcher = MyDispatcher() - listener = Listener() - dispatcher.push_handlers(listener) - -In this example both `listener.on_resize` and `listener.on_keypress` are -registered as handlers for respective events, but `listener.other_method` is -not affected, because it doesn't correspond to a registered event type. - -Finally, yet another option is to subclass the dispatcher and override the event -handler methods:: - - class MyDispatcher(pyglet.event.EventDispatcher): + class MyDispatcher(DispatcherClass): def on_resize(self, width, height): # ... -If both a parent class and the child class have a handler for the same event, -only the child's version of the method is invoked. If both event handlers are -needed, the child's handler must explicitly call the parent's handler: +Event handler stack +=================== - class ParentDispatcher(pyglet.event.EventDispatcher): - def on_resize(self, w, h); - # ... +When attaching an event handler to a dispatcher using the above methods, it +replaces any existing handler (causing the original handler to no longer be +called). Each dispatcher maintains a stack of event handlers, allowing you to +insert an event handler "above" the existing one rather than replacing it. - class ChildDispatcher(ParentDispatcher): - def on_resize(self, w, h): - super().on_resize(w, h) - # ... +There are two main use cases for "pushing" event handlers: -Multiple handlers for an event -============================== +* Temporarily intercepting the events coming from the dispatcher by pushing a + custom set of handlers onto the dispatcher, then later "popping" them all + off at once. +* Creating "chains" of event handlers, where the event propagates from the + top-most (most recently added) handler to the bottom, until a handler + takes care of it. -A single event can be handled by multiple handlers. The handlers are invoked in -the order opposite to the order of their registration. So, the handler -registered last will be the first to be invoke when the event is fired. +Use `EventDispatcher.push_handlers` to create a new level in the stack and +attach handlers to it. You can push several handlers at once:: -An event handler can return the value `pyglet.event.EVENT_HANDLED` to prevent -running the subsequent handlers. Alternatively if the handle returns -`pyglet.event.EVENT_UNHANDLED` or doesn't return an explicit value, the next -event handler will be called (if there is one). + dispatcher.push_handlers(on_resize, on_key_press) -Stopping the event propagation is useful to prevent a single user action from -being handled by two unrelated systems. For instance, in game using WASD keys -for movement, should suppress movement when a chat window is opened: the -"keypress" event should be handled by the chat or by the character -movement system, but not both. +If your function handlers have different names to the events they handle, use +keyword arguments:: -The order of execution of event handlers can be changed by assigning them -priority. Default priority for all handlers is 0. If handler needs to be run -before other handlers even though it was added early, it can be assigned -priority 1. Conversely, a handler added late can be assigned priority -1 to be -run late. + dispatcher.push_handlers(on_resize=my_resize, on_key_press=my_key_press) -Priority can be assigned by passing the `priority` named parameter to -`push_handlers` method: +After an event handler has processed an event, it is passed on to the +next-lowest event handler, unless the handler returns `EVENT_HANDLED`, which +prevents further propagation. - window.push_handlers(on_resize, priority=-1) +To remove all handlers on the top stack level, use +`EventDispatcher.pop_handlers`. -It can also be specified by using the `@priority` decorators on handler -functions and methods: +Note that any handlers pushed onto the stack have precedence over the +handlers set directly on the instance (for example, using the methods +described in the previous section), regardless of when they were set. +For example, handler ``foo`` is called before handler ``bar`` in the following +example:: - @pyglet.event.priority(1) - def on_resize(w, h): - # ... + dispatcher.push_handlers(on_resize=foo) + dispatcher.on_resize = bar - class Listener(object): - @pyglet.event.priority(-1) - def on_resize(self, w, h): - # ... - listener = Listener() +Dispatching events +================== - dispatcher.push_handlers(on_resize, listener) +pyglet uses a single-threaded model for all application code. Event +handlers are only ever invoked as a result of calling +EventDispatcher.dispatch_events`. -Removing event handlers -======================= +It is up to the specific event dispatcher to queue relevant events until they +can be dispatched, at which point the handlers are called in the order the +events were originally generated. -In most cases it is not necessary to remove event handlers manually. When -the handler is an object method, the event dispatcher keeps only a weak -reference to it. It means, that the dispatcher will not prevent the object from -being deleted when it goes out of scope. In that case the handler will be -silently removed from the list of handlers. +This implies that your application runs with a main loop that continuously +updates the application state and checks for new events:: + + while True: + dispatcher.dispatch_events() + # ... additional per-frame processing + +Not all event dispatchers require the call to ``dispatch_events``; check with +the particular class documentation. .. note:: - This means the following example will not work, because the pushed object - will fall out of scope and be collected:: + In order to prevent issues with garbage collection, the + :py:class:`~pyglet.event.EventDispatcher` class only holds weak + references to pushed event handlers. That means the following example + will not work, because the pushed object will fall out of scope and be + collected:: dispatcher.push_handlers(MyHandlerClass()) @@ -202,50 +115,12 @@ silently removed from the list of handlers. my_handler_instance = MyHandlerClass() dispatcher.push_handlers(my_handler_instance) -When explicit removal of handlers is required, the method `remove_handlers` -can be used. Its arguments are the same as the arguments of `push_handlers`: - - dispatcher.remove_handlers(on_resize) - dispatcher.remove_handlers(on_resize=my_handler) - dispatcher.remove_handlers(on_resize=obj.my_handler) - dispatcher.remove_handlers(obj) - -When an object is passed as a positional parameter to `remove_handlers`, all its -methods are removed from the handlers, regardless of their names. - -Dispatching events -================== - -pyglet uses a single-threaded model for all application code. Normally event -handlers are invoked while running an event loop by calling - - pyglet.app.run() - -or - - event_loop = pyglet.app.EventLoop() - event_loop.run() - -Application code can invoke events directly by calling the method -`dispatch_event` of `EventDispatcher`: - - dispatcher.dispatch_event('on_resize', 640, 480) - -The first argument of this method is the event name, that has to be previously -registered using `register_event_type` class method. The rest of the arguments -are pass to event handlers. - -The handlers of an event fired by calling `dispatch_event` are called directly -from this method. If any of the handlers returns `EVENT_HANDLED`, then -`dispatch_event` also returns `EVENT_HANDLED` otherwise (or if there weren't -any handlers for a given event) it returns `EVENT_UNHANDLED`. """ -import inspect as _inspect - -from functools import partial as _partial -from weakref import WeakMethod as _WeakMethod +import inspect +from functools import partial +from weakref import WeakMethod EVENT_HANDLED = True EVENT_UNHANDLED = None @@ -257,36 +132,17 @@ class EventException(Exception): pass -def priority(prio=0): - """A decorator to set priority on handler functions and handlers. - - Default priority is 0. Handlers with higher priority are invoked first. - Recommended priority values are 1 and -1. In most cases more than 3 priority - classes are not required. - """ - def wrap(func): - func.__priority = prio - return func - return wrap - - class EventDispatcher: """Generic event dispatcher interface. See the module docstring for usage. """ - # This field will contain the queues of event handlers for every supported - # event type. It is lazily initialized when the first event handler is added - # to the class. After that it contains a dictionary of lists, in which - # handlers are sorted according to their priority: - # {'on_event': [(priority1, handler1), - # (priority2, handler2)]} - # Handlers are invoked until any one of them returns EVENT_HANDLED - _handlers = None + # Placeholder empty stack; real stack is created only if needed + _event_stack = () @classmethod def register_event_type(cls, name): - """Registers an event type with the dispatcher. + """Register an event type with the dispatcher. Registering event types allows the dispatcher to validate event handler names as they are attached, and to search attached objects for @@ -295,216 +151,192 @@ class EventDispatcher: :Parameters: `name` : str Name of the event to register. + """ if not hasattr(cls, 'event_types'): cls.event_types = [] cls.event_types.append(name) + return name - def _get_names_from_handler(self, handler): - """Yields event names handled by a handler function, method or object. + def push_handlers(self, *args, **kwargs): + """Push a level onto the top of the handler stack, then attach zero or + more event handlers. + + If keyword arguments are given, they name the event type to attach. + Otherwise, a callable's `__name__` attribute will be used. Any other + object may also be specified, in which case it will be searched for + callables with event names. """ - if callable(handler) and hasattr(handler, '__name__'): - # Take the name of a function or a method. - yield handler.__name__ - else: - # Iterate through all the methods of an object and yield those that - # match registered events. - for name in dir(handler): - if name in self.event_types and callable(getattr(handler, name)): - yield name + # Create event stack if necessary + if type(self._event_stack) is tuple: + self._event_stack = [] - def _finalize_weak_method(self, name, weak_method): - """Called to remove dead WeakMethods from handlers.""" - handlers = self._handlers[name] - i = 0 - # This is not the most efficient way of removing several elements from - # an array, but in almost all cases only one element has to be removed. - while i < len(handlers): - if handlers[i][1] is weak_method: - del handlers[i] + # Place dict full of new handlers at beginning of stack + self._event_stack.insert(0, {}) + self.set_handlers(*args, **kwargs) + + def _get_handlers(self, args, kwargs): + """Implement handler matching on arguments for set_handlers and + remove_handlers. + """ + for obj in args: + if inspect.isroutine(obj): + # Single magically named function + name = obj.__name__ + if name not in self.event_types: + raise EventException(f'Unknown event "{name}"') + if inspect.ismethod(obj): + yield name, WeakMethod(obj, partial(self._remove_handler, name)) + else: + yield name, obj else: - i += 1 - - @staticmethod - def _remove_handler_from_queue(handlers_queue, handler): - """Remove all instances of a handler from a queue for a single event. - - If `handler` is an object, then all the methods bound to this object - will be removed from the queue. - """ - i = 0 - # This is not the most efficient way of removing several elements from - # an array, but in almost all cases only one element has to be removed. - while i < len(handlers_queue): - _, registered_handler = handlers_queue[i] - if isinstance(registered_handler, _WeakMethod): - # Wrapped in _WeakMethod in `push_handler`. - registered_handler = registered_handler() - if registered_handler is handler or getattr(registered_handler, '__self__', None) is handler: - del handlers_queue[i] - else: - i += 1 - - def push_handler(self, name, handler, priority=None): - """Adds a single event handler. - - If the `handler` parameter is callable, it will be registered directly. - Otherwise, it's expected to be an object having a method with a name - matching the name of the event. - - If the `priority` parameter is not None, it is used as a priotity. - Otherwise, the value specified by the @priority decorator is used. If - neither is specified the default value of 0 is used. - """ - if not hasattr(self.__class__, 'event_types'): - self.__class__.event_types = [] - if name not in self.event_types: - raise EventException('Unknown event "{}"'.format(name)) - if not callable(handler): - # If handler is not callable, search for in it for a method with - # a name matching the name of the event. - if hasattr(handler, name): - method = getattr(handler, name) - if not callable(method): - raise EventException( - 'Field {} on "{}" is not callable'.format( - name, repr(handler))) - handler = method - else: - raise EventException( - '"{}" is not callable and doesn\'t have ' - 'a method "{}"'.format(repr(handler), name)) - - # Determine priority - if priority is None: - priority = getattr(handler, '__priority', 0) - # A hack for the case when handler is a MagicMock. - if type(priority) not in (int, float): - priority = int(priority) - - # Wrap methods in weak references. - if _inspect.ismethod(handler): - handler = _WeakMethod(handler, _partial(self._finalize_weak_method, name)) - - # Create handler queues if necessary. - if self._handlers is None: - self._handlers = {} - self.push_handlers(self) - if name not in self._handlers: - self._handlers[name] = [] - - handlers = self._handlers[name] - - # Finding the place to insert the new handler. All the previous handlers - # have to have strictly higher priority. - # - # A binary search would theoretically be faster, but a) there's - # usually just a handful of handlers, b) we are going to shift - # the elements in the list anyway, which will take O(n), c) we are - # doing this only during handler registration, and we are more - # conserned in the efficiency of dispatching event. - i = 0 - while i < len(handlers) and handlers[i][0] > priority: - i += 1 - - handlers.insert(i, (priority, handler)) - - def push_handlers(self, *args, priority=None, **kwargs): - """Adds new handlers to registered events. - - Multiple positional and keyword arguments can be provided. - - For a keyword argument, the name of the event is taken from the name - of the argument. If the argument is callable, it is used - as a handler directly. If the argument is an object, it is searched for - a method with the name matching the name of the event/argument. - - When a callable named object (usually a function or a method) is passed - as a positional argument, its name is used as the event name. When - an object is passed as a positional argument, it is scanned for methods - with names that match the names of registered events. These methods are - added as handlers for the respective events. - - An optional argument priority can be used to override the priority for - all the added handlers. Default priority is 0, and handlers with higher - priority will be invoked first. The priority specified in the call will - take precedence of priority, specified in @priority decorator. - - EventException is raised if the event name is not registered. - """ - if not hasattr(self.__class__, 'event_types'): - self.__class__.event_types = [] - - for handler in args: - for name in self._get_names_from_handler(handler): - self.push_handler(name, handler, priority=priority) + # Single instance with magically named methods + for name in dir(obj): + if name in self.event_types: + meth = getattr(obj, name) + yield name, WeakMethod(meth, partial(self._remove_handler, name)) for name, handler in kwargs.items(): - self.push_handler(name, handler, priority) + # Function for handling given event (no magic) + if name not in self.event_types: + raise EventException(f'Unknown event "{name}"') + if inspect.ismethod(handler): + yield name, WeakMethod(handler, partial(self._remove_handler, name)) + else: + yield name, handler - def remove_handler(self, name_or_handler=None, handler=None, name=None): - """Removes a single event handler. + def set_handlers(self, *args, **kwargs): + """Attach one or more event handlers to the top level of the handler + stack. - Can be called in one of the following ways: - - dispatcher.remove_handler(my_handler) - dispatcher.remove_handler(handler=my_handler) - dispatcher.remove_handler("event_name", my_handler) - dispatcher.remove_handler(name="event_name", handler=my_handler) - - If the event name is specified, only the queue of handlers for that - event is scanned, and the handler is removed from it. Otherwise all - handler queues are scanned and the handler is removed from all of them. - - If the handler is an object, then all the registered handlers that are - bound to this object are removed. Unlike `push_handler`, the method - names in the class are not taken into account. - - No error is raised if the event handler is not set. + See :py:meth:`~pyglet.event.EventDispatcher.push_handlers` for the accepted argument types. """ - if handler is None: - # Called with one positional argument (example #1) - assert name is None - assert name_or_handler is not None - handler = name_or_handler - elif name is not None: - # Called with keyword arguments for handler and name (example #4) - assert name_or_handler is None - else: - # Called with two positional arguments, or only with handler as - # a keyword argument (examples #2, #3) - name = name_or_handler + # Create event stack if necessary + if type(self._event_stack) is tuple: + self._event_stack = [{}] - if name is not None: - if name in self._handlers: - self._remove_handler_from_queue(self._handlers[name], handler) - else: - for handlers_queue in self._handlers.values(): - self._remove_handler_from_queue(handlers_queue, handler) + for name, handler in self._get_handlers(args, kwargs): + self.set_handler(name, handler) + + def set_handler(self, name, handler): + """Attach a single event handler. + + :Parameters: + `name` : str + Name of the event type to attach to. + `handler` : callable + Event handler to attach. + + """ + # Create event stack if necessary + if type(self._event_stack) is tuple: + self._event_stack = [{}] + + self._event_stack[0][name] = handler + + def pop_handlers(self): + """Pop the top level of event handlers off the stack. + """ + assert self._event_stack and 'No handlers pushed' + + del self._event_stack[0] def remove_handlers(self, *args, **kwargs): - """Removes event handlers from the event handlers queue. + """Remove event handlers from the event stack. See :py:meth:`~pyglet.event.EventDispatcher.push_handlers` for the - accepted argument types. Handlers, passed as positional arguments - are removed from all events, regardless of their names. + accepted argument types. All handlers are removed from the first stack + frame that contains any of the given handlers. No error is raised if + any handler does not appear in that frame, or if no stack frame + contains any of the given handlers. - No error is raised if any handler does not appear among - the registered handlers. + If the stack frame is empty after removing the handlers, it is + removed from the stack. Note that this interferes with the expected + symmetry of :py:meth:`~pyglet.event.EventDispatcher.push_handlers` and + :py:meth:`~pyglet.event.EventDispatcher.pop_handlers`. """ - for handler in args: - self.remove_handler(None, handler) + handlers = list(self._get_handlers(args, kwargs)) - for name, handler in kwargs.items(): - self.remove_handler(name, handler) + # Find the first stack frame containing any of the handlers + def find_frame(): + for frame in self._event_stack: + for name, handler in handlers: + try: + if frame[name] == handler: + return frame + except KeyError: + pass + + frame = find_frame() + + # No frame matched; no error. + if not frame: + return + + # Remove each handler from the frame. + for name, handler in handlers: + try: + if frame[name] == handler: + del frame[name] + except KeyError: + pass + + # Remove the frame if it's empty. + if not frame: + self._event_stack.remove(frame) + + def remove_handler(self, name, handler): + """Remove a single event handler. + + The given event handler is removed from the first handler stack frame + it appears in. The handler must be the exact same callable as passed + to `set_handler`, `set_handlers` or + :py:meth:`~pyglet.event.EventDispatcher.push_handlers`; and the name + must match the event type it is bound to. + + No error is raised if the event handler is not set. + + :Parameters: + `name` : str + Name of the event type to remove. + `handler` : callable + Event handler to remove. + """ + for frame in self._event_stack: + try: + if frame[name] == handler: + del frame[name] + break + except KeyError: + pass + + def _remove_handler(self, name, handler): + """Used internally to remove all handler instances for the given event name. + + This is normally called from a dead ``WeakMethod`` to remove itself from the + event stack. + """ + + # Iterate over a copy as we might mutate the list + for frame in list(self._event_stack): + + if name in frame: + try: + if frame[name] == handler: + del frame[name] + if not frame: + self._event_stack.remove(frame) + except TypeError: + # weakref is already dead + pass def dispatch_event(self, event_type, *args): """Dispatch a single event to the attached handlers. - The event is propagated to all handlers from the top of the stack + The event is propagated to all handlers from from the top of the stack until one returns `EVENT_HANDLED`. This method should be used only by - :py:class:`~pyglet.event.EventDispatcher` implementors; applications - should call the ``dispatch_events`` method. + :py:class:`~pyglet.event.EventDispatcher` implementors; applications should call + the ``dispatch_events`` method. Since pyglet 1.2, the method returns `EVENT_HANDLED` if an event handler returned `EVENT_HANDLED` or `EVENT_UNHANDLED` if all events @@ -525,32 +357,48 @@ class EventDispatcher: is always ``None``. """ - if not hasattr(self.__class__, 'event_types'): - self.__class__.event_types = [] - if event_type not in self.event_types: - raise EventException( - 'Attempted to dispatch an event of unknown event type "{}". ' - 'Event types have to be registered by calling ' - 'DispatcherClass.register_event_type({})'.format( - event_type, repr(event_type))) + assert hasattr(self, 'event_types'), ( + "No events registered on this EventDispatcher. " + "You need to register events with the class method " + "EventDispatcher.register_event_type('event_name')." + ) + assert event_type in self.event_types, \ + f"{event_type} not found in {self}.event_types == {self.event_types}" - if self._handlers is None: - # Initialize the handlers with the object itself. - self._handlers = {} - self.push_handlers(self) + invoked = False - handlers_queue = self._handlers.get(event_type, ()) - for _, handler in handlers_queue: - if isinstance(handler, _WeakMethod): + # Search handler stack for matching event handlers + for frame in list(self._event_stack): + handler = frame.get(event_type, None) + if not handler: + continue + if isinstance(handler, WeakMethod): handler = handler() assert handler is not None try: + invoked = True if handler(*args): return EVENT_HANDLED except TypeError as exception: self._raise_dispatch_exception(event_type, args, handler, exception) - return EVENT_UNHANDLED + # Check instance for an event handler + try: + if getattr(self, event_type)(*args): + return EVENT_HANDLED + except AttributeError as e: + event_op = getattr(self, event_type, None) + if callable(event_op): + raise e + except TypeError as exception: + self._raise_dispatch_exception(event_type, args, getattr(self, event_type), exception) + else: + invoked = True + + if invoked: + return EVENT_UNHANDLED + + return False @staticmethod def _raise_dispatch_exception(event_type, args, handler, exception): @@ -566,7 +414,7 @@ class EventDispatcher: n_args = len(args) # Inspect the handler - argspecs = _inspect.getfullargspec(handler) + argspecs = inspect.getfullargspec(handler) handler_args = argspecs.args handler_varargs = argspecs.varargs handler_defaults = argspecs.defaults @@ -574,7 +422,7 @@ class EventDispatcher: n_handler_args = len(handler_args) # Remove "self" arg from handler if it's a bound method - if _inspect.ismethod(handler) and handler.__self__: + if inspect.ismethod(handler) and handler.__self__: n_handler_args -= 1 # Allow *args varargs to overspecify arguments @@ -586,15 +434,14 @@ class EventDispatcher: n_handler_args = n_args if n_handler_args != n_args: - if _inspect.isfunction(handler) or _inspect.ismethod(handler): - descr = "'%s' at %s:%d" % (handler.__name__, - handler.__code__.co_filename, - handler.__code__.co_firstlineno) + if inspect.isfunction(handler) or inspect.ismethod(handler): + descr = f"'{handler.__name__}' at {handler.__code__.co_filename}:{handler.__code__.co_firstlineno}" else: descr = repr(handler) - raise TypeError(f"The '{event_type}' event was dispatched with {len(args)} arguments, " - f"but your handler {descr} accepts only {len(handler_args)} arguments.") + raise TypeError(f"The '{event_type}' event was dispatched with {len(args)} arguments,\n" + f"but your handler {descr} accepts only {n_handler_args} arguments.") + else: raise exception @@ -616,22 +463,23 @@ class EventDispatcher: # ... """ - if len(args) == 0: # @window.event() + if len(args) == 0: # @window.event() def decorator(func): - name = func.__name__ - self.push_handler(name, func) + func_name = func.__name__ + self.set_handler(func_name, func) return func - return decorator - elif _inspect.isroutine(args[0]): # @window.event + return decorator + elif inspect.isroutine(args[0]): # @window.event func = args[0] name = func.__name__ - self.push_handler(name, func) + self.set_handler(name, func) return args[0] - - elif isinstance(args[0], str): # @window.event('on_resize') + elif isinstance(args[0], str): # @window.event('on_resize') name = args[0] + def decorator(func): - self.push_handler(name, func) + self.set_handler(name, func) return func + return decorator diff --git a/libs/pyglet/image/__init__.py b/libs/pyglet/image/__init__.py index 4a0bc05..02678f4 100644 --- a/libs/pyglet/image/__init__.py +++ b/libs/pyglet/image/__init__.py @@ -1426,12 +1426,6 @@ class Texture(AbstractImage): order = self.tex_coords_order self.tex_coords_order = (order[bl], order[br], order[tr], order[tl]) - @property - def uv(self): - """Tuple containing the left, bottom, right, top 2D texture coordinates.""" - tex_coords = self.tex_coords - return tex_coords[0], tex_coords[1], tex_coords[3], tex_coords[7] - def __repr__(self): return "{}(id={}, size={}x{})".format(self.__class__.__name__, self.id, self.width, self.height) diff --git a/libs/pyglet/sprite.py b/libs/pyglet/sprite.py index 35ee439..31ad3e2 100644 --- a/libs/pyglet/sprite.py +++ b/libs/pyglet/sprite.py @@ -78,32 +78,16 @@ from pyglet import image _is_pyglet_doc_run = hasattr(sys, "is_pyglet_doc_run") and sys.is_pyglet_doc_run -vertex_source = """#version 150 +vertex_source = """#version 150 core + in vec3 translate; + in vec4 colors; + in vec3 tex_coords; + in vec2 scale; in vec3 position; - in vec4 size; - in vec4 color; - in vec4 texture_uv; in float rotation; - out vec4 geo_size; - out vec4 geo_color; - out vec4 geo_tex_coords; - out float geo_rotation; - - void main() { - gl_Position = vec4(position, 1); - geo_size = size; - geo_color = color; - geo_tex_coords = texture_uv; - geo_rotation = rotation; - } -""" - -geometry_source = """#version 150 - // We are taking single points form the vertex shader - // and emitting 4 new vertices creating a quad/sprites - layout (points) in; - layout (triangle_strip, max_vertices = 4) out; + out vec4 vertex_colors; + out vec3 texture_coords; uniform WindowBlock { @@ -111,102 +95,52 @@ geometry_source = """#version 150 mat4 view; } window; + mat4 m_scale = mat4(1.0); + mat4 m_rotation = mat4(1.0); + mat4 m_translate = mat4(1.0); - // Since geometry shader can take multiple values from a vertex - // shader we need to define the inputs from it as arrays. - // In our instance we just take single values (points) - in vec4 geo_size[]; - in vec4 geo_color[]; - in vec4 geo_tex_coords[]; - in float geo_rotation[]; + void main() + { + m_scale[0][0] = scale.x; + m_scale[1][1] = scale.y; + m_translate[3][0] = translate.x; + m_translate[3][1] = translate.y; + m_translate[3][2] = translate.z; + m_rotation[0][0] = cos(-radians(rotation)); + m_rotation[0][1] = sin(-radians(rotation)); + m_rotation[1][0] = -sin(-radians(rotation)); + m_rotation[1][1] = cos(-radians(rotation)); - out vec2 uv; - out vec4 frag_color; + gl_Position = window.projection * window.view * m_translate * m_rotation * m_scale * vec4(position, 1.0); - void main() { - - // We grab the position value from the vertex shader - vec2 center = gl_in[0].gl_Position.xy; - - // Calculate the half size of the sprites for easier calculations - vec2 hsize = geo_size[0].xy / 2.0; - - // Convert the rotation to radians - float angle = radians(-geo_rotation[0]); - - // Create a scale vector - vec2 scale = vec2(geo_size[0][2], geo_size[0][3]); - - // Create a 2d rotation matrix - mat2 rot = mat2(cos(angle), sin(angle), - -sin(angle), cos(angle)); - - // Calculate the left, bottom, right, top: - float tl = geo_tex_coords[0].s; - float tb = geo_tex_coords[0].t; - float tr = geo_tex_coords[0].s + geo_tex_coords[0].p; - float tt = geo_tex_coords[0].t + geo_tex_coords[0].q; - - // Emit a triangle strip creating a quad (4 vertices). - // Here we need to make sure the rotation is applied before we position the sprite. - // We just use hardcoded texture coordinates here. If an atlas is used we - // can pass an additional vec4 for specific texture coordinates. - // Each EmitVertex() emits values down the shader pipeline just like a single - // run of a vertex shader, but in geomtry shaders we can do it multiple times! - - // Upper left - gl_Position = window.projection * window.view * vec4(rot * vec2(-hsize.x, hsize.y) * scale + center, 0.0, 1.0); - uv = vec2(tl, tt); - frag_color = geo_color[0]; - EmitVertex(); - - // lower left - gl_Position = window.projection * window.view * vec4(rot * vec2(-hsize.x, -hsize.y) * scale + center, 0.0, 1.0); - uv = vec2(tl, tb); - frag_color = geo_color[0]; - EmitVertex(); - - // upper right - gl_Position = window.projection * window.view * vec4(rot * vec2(hsize.x, hsize.y) * scale + center, 0.0, 1.0); - uv = vec2(tr, tt); - frag_color = geo_color[0]; - EmitVertex(); - - // lower right - gl_Position = window.projection * window.view * vec4(rot * vec2(hsize.x, -hsize.y) * scale + center, 0.0, 1.0); - uv = vec2(tr, tb); - frag_color = geo_color[0]; - EmitVertex(); - - // We are done with this triangle strip now - EndPrimitive(); + vertex_colors = colors; + texture_coords = tex_coords; } """ -fragment_source = """#version 150 - in vec2 uv; - in vec4 frag_color; - out vec4 final_color; +fragment_source = """#version 150 core + in vec4 vertex_colors; + in vec3 texture_coords; + out vec4 final_colors; uniform sampler2D sprite_texture; - void main() { - final_color = texture(sprite_texture, uv) * frag_color; + void main() + { + final_colors = texture(sprite_texture, texture_coords.xy) * vertex_colors; } - """ fragment_array_source = """#version 150 core - in vec2 uv; - in vec4 frag_color; - + in vec4 vertex_colors; + in vec3 texture_coords; out vec4 final_colors; uniform sampler2DArray sprite_texture; void main() { - final_colors = texture(sprite_texture, uv) * frag_color; + final_colors = texture(sprite_texture, texture_coords) * vertex_colors; } """ @@ -215,10 +149,9 @@ def get_default_shader(): try: return pyglet.gl.current_context.pyglet_sprite_default_shader except AttributeError: - vert_shader = graphics.shader.Shader(vertex_source, 'vertex') - geom_shader = graphics.shader.Shader(geometry_source, 'geometry') - frag_shader = graphics.shader.Shader(fragment_source, 'fragment') - default_shader_program = graphics.shader.ShaderProgram(vert_shader, geom_shader, frag_shader) + _default_vert_shader = graphics.shader.Shader(vertex_source, 'vertex') + _default_frag_shader = graphics.shader.Shader(fragment_source, 'fragment') + default_shader_program = graphics.shader.ShaderProgram(_default_vert_shader, _default_frag_shader) pyglet.gl.current_context.pyglet_sprite_default_shader = default_shader_program return pyglet.gl.current_context.pyglet_sprite_default_shader @@ -227,10 +160,9 @@ def get_default_array_shader(): try: return pyglet.gl.current_context.pyglet_sprite_default_array_shader except AttributeError: - vert_shader = graphics.shader.Shader(vertex_source, 'vertex') - geom_shader = graphics.shader.Shader(geometry_source, 'geometry') - frag_shader = graphics.shader.Shader(fragment_array_source, 'fragment') - default_shader_program = graphics.shader.ShaderProgram(vert_shader, geom_shader, frag_shader) + _default_vert_shader = graphics.shader.Shader(vertex_source, 'vertex') + _default_array_frag_shader = graphics.shader.Shader(fragment_array_source, 'fragment') + default_shader_program = graphics.shader.ShaderProgram(_default_vert_shader, _default_array_frag_shader) pyglet.gl.current_context.pyglet_sprite_default_array_shader = default_shader_program return pyglet.gl.current_context.pyglet_sprite_default_array_shader @@ -312,7 +244,8 @@ class Sprite(event.EventDispatcher): _frame_index = 0 _paused = False _rotation = 0 - _rgba = [255, 255, 255, 255] + _opacity = 255 + _rgb = (255, 255, 255) _scale = 1.0 _scale_x = 1.0 _scale_y = 1.0 @@ -326,8 +259,7 @@ class Sprite(event.EventDispatcher): blend_dest=GL_ONE_MINUS_SRC_ALPHA, batch=None, group=None, - subpixel=False, - program=None): + subpixel=False): """Create a sprite. :Parameters: @@ -352,11 +284,6 @@ class Sprite(event.EventDispatcher): `subpixel` : bool Allow floating-point coordinates for the sprite. By default, coordinates are restricted to integer values. - `program` : `~pyglet.graphics.shader.ShaderProgram` - A custom shader to use. This shader program must contain the - exact same attribute names and types as the default shader. - The class methods and properties depend on this, and will - crash otherwise. """ self._x = x self._y = y @@ -372,46 +299,26 @@ class Sprite(event.EventDispatcher): else: self._texture = img.get_texture() - if not program: - if isinstance(img, image.TextureArrayRegion): - self._program = get_default_array_shader() - else: - self._program = get_default_shader() - else: - self._program = program - self._batch = batch or graphics.get_default_batch() - self._user_group = group self._group = self.group_class(self._texture, blend_src, blend_dest, self.program, group) self._subpixel = subpixel - self._create_vertex_list() - def _create_vertex_list(self): - texture = self._texture - self._vertex_list = self.program.vertex_list( - 1, GL_POINTS, self._batch, self._group, - position=('f', (self._x, self._y, self._z)), - size=('f', (texture.width, texture.height, 1, 1)), - color=('Bn', self._rgba), - texture_uv=('f', texture.uv), - rotation=('f', (self._rotation,))) - @property def program(self): - return self._program + if isinstance(self._img, image.TextureArrayRegion): + program = get_default_array_shader() + else: + program = get_default_shader() - @program.setter - def program(self, program): - if self._program == program: - return - self._group = self.group_class(self._texture, - self._group.blend_src, - self._group.blend_dest, - program, - self._user_group) - self._batch.migrate(self._vertex_list, GL_POINTS, self._group, self._batch) - self._program = program + return program + + def __del__(self): + try: + if self._vertex_list is not None: + self._vertex_list.delete() + except: + pass def delete(self): """Force immediate removal of the sprite from video memory. @@ -425,6 +332,8 @@ class Sprite(event.EventDispatcher): self._vertex_list.delete() self._vertex_list = None self._texture = None + + # Easy way to break circular reference, speeds up GC self._group = None def _animate(self, dt): @@ -464,7 +373,7 @@ class Sprite(event.EventDispatcher): return if batch is not None and self._batch is not None: - self._batch.migrate(self._vertex_list, GL_POINTS, self._group, batch) + self._batch.migrate(self._vertex_list, GL_TRIANGLES, self._group, batch) self._batch = batch else: self._vertex_list.delete() @@ -491,7 +400,7 @@ class Sprite(event.EventDispatcher): self._group.blend_dest, self._group.program, group) - self._batch.migrate(self._vertex_list, GL_POINTS, self._group, self._batch) + self._batch.migrate(self._vertex_list, GL_TRIANGLES, self._group, self._batch) @property def image(self): @@ -519,6 +428,7 @@ class Sprite(event.EventDispatcher): clock.schedule_once(self._animate, self._next_dt) else: self._set_texture(img.get_texture()) + self._update_position() def _set_texture(self, texture): if texture.id is not self._texture.id: @@ -531,9 +441,35 @@ class Sprite(event.EventDispatcher): self._texture = texture self._create_vertex_list() else: - self._vertex_list.texture_uv[:] = texture.uv + self._vertex_list.tex_coords[:] = texture.tex_coords self._texture = texture + def _create_vertex_list(self): + self._vertex_list = self.program.vertex_list_indexed( + 4, GL_TRIANGLES, [0, 1, 2, 0, 2, 3], self._batch, self._group, + colors=('Bn', (*self._rgb, int(self._opacity)) * 4), + translate=('f', (self._x, self._y, self._z) * 4), + scale=('f', (self._scale*self._scale_x, self._scale*self._scale_y) * 4), + rotation=('f', (self._rotation,) * 4), + tex_coords=('f', self._texture.tex_coords)) + self._update_position() + + def _update_position(self): + if not self._visible: + self._vertex_list.position[:] = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + else: + img = self._texture + x1 = -img.anchor_x + y1 = -img.anchor_y + x2 = x1 + img.width + y2 = y1 + img.height + vertices = (x1, y1, 0, x2, y1, 0, x2, y2, 0, x1, y2, 0) + + if not self._subpixel: + self._vertex_list.position[:] = tuple(map(int, vertices)) + else: + self._vertex_list.position[:] = vertices + @property def position(self): """The (x, y, z) coordinates of the sprite, as a tuple. @@ -551,7 +487,7 @@ class Sprite(event.EventDispatcher): @position.setter def position(self, position): self._x, self._y, self._z = position - self._vertex_list.position[:] = position + self._vertex_list.translate[:] = position * 4 @property def x(self): @@ -564,7 +500,7 @@ class Sprite(event.EventDispatcher): @x.setter def x(self, x): self._x = x - self._vertex_list.position[:] = x, self._y, self._z + self._vertex_list.translate[:] = (x, self._y, self._z) * 4 @property def y(self): @@ -577,7 +513,7 @@ class Sprite(event.EventDispatcher): @y.setter def y(self, y): self._y = y - self._vertex_list.position[:] = self._x, y, self._z + self._vertex_list.translate[:] = (self._x, y, self._z) * 4 @property def z(self): @@ -590,7 +526,7 @@ class Sprite(event.EventDispatcher): @z.setter def z(self, z): self._z = z - self._vertex_list.position[:] = self._x, self._y, z + self._vertex_list.translate[:] = (self._x, self._y, z) * 4 @property def rotation(self): @@ -606,7 +542,7 @@ class Sprite(event.EventDispatcher): @rotation.setter def rotation(self, rotation): self._rotation = rotation - self._vertex_list.rotation[0] = self._rotation + self._vertex_list.rotation[:] = (self._rotation,) * 4 @property def scale(self): @@ -622,7 +558,7 @@ class Sprite(event.EventDispatcher): @scale.setter def scale(self, scale): self._scale = scale - self._vertex_list.scale[:] = scale * self._scale_x, scale * self._scale_y + self._vertex_list.scale[:] = (scale * self._scale_x, scale * self._scale_y) * 4 @property def scale_x(self): @@ -638,7 +574,7 @@ class Sprite(event.EventDispatcher): @scale_x.setter def scale_x(self, scale_x): self._scale_x = scale_x - self._vertex_list.scale[:] = self._scale * scale_x, self._scale * self._scale_y + self._vertex_list.scale[:] = (self._scale * scale_x, self._scale * self._scale_y) * 4 @property def scale_y(self): @@ -654,7 +590,7 @@ class Sprite(event.EventDispatcher): @scale_y.setter def scale_y(self, scale_y): self._scale_y = scale_y - self._vertex_list.scale[:] = self._scale * self._scale_x, self._scale * scale_y + self._vertex_list.scale[:] = (self._scale * self._scale_x, self._scale * scale_y) * 4 def update(self, x=None, y=None, z=None, rotation=None, scale=None, scale_x=None, scale_y=None): """Simultaneously change the position, rotation or scale. @@ -693,11 +629,11 @@ class Sprite(event.EventDispatcher): translations_outdated = True if translations_outdated: - self._vertex_list.position[:] = (self._x, self._y, self._z) + self._vertex_list.translate[:] = (self._x, self._y, self._z) * 4 if rotation is not None and rotation != self._rotation: self._rotation = rotation - self._vertex_list.rotation[:] = rotation + self._vertex_list.rotation[:] = (rotation,) * 4 scales_outdated = False @@ -713,7 +649,7 @@ class Sprite(event.EventDispatcher): scales_outdated = True if scales_outdated: - self._vertex_list.scale[:] = self._scale * self._scale_x, self._scale * self._scale_y + self._vertex_list.scale[:] = (self._scale * self._scale_x, self._scale * self._scale_y) * 4 @property def width(self): @@ -755,12 +691,12 @@ class Sprite(event.EventDispatcher): :type: int """ - return self._rgba[3] + return self._opacity @opacity.setter def opacity(self, opacity): - self._rgba[3] = opacity - self._vertex_list.color[:] = self._rgba + self._opacity = opacity + self._vertex_list.colors[:] = (*self._rgb, int(self._opacity)) * 4 @property def color(self): @@ -774,12 +710,12 @@ class Sprite(event.EventDispatcher): :type: (int, int, int) """ - return self._rgba[:3] + return self._rgb @color.setter def color(self, rgb): - self._rgba[:3] = list(map(int, rgb)) - self._vertex_list.color[:] = self._rgba + self._rgb = list(map(int, rgb)) + self._vertex_list.colors[:] = (*self._rgb, int(self._opacity)) * 4 @property def visible(self): @@ -792,7 +728,7 @@ class Sprite(event.EventDispatcher): @visible.setter def visible(self, visible): self._visible = visible - self._vertex_list.texture_uv[:] = (0, 0, 0, 0) if not visible else self._texture.uv + self._update_position() @property def paused(self): @@ -846,16 +782,9 @@ class Sprite(event.EventDispatcher): efficiently. """ self._group.set_state_recursive() - self._vertex_list.draw(GL_POINTS) + self._vertex_list.draw(GL_TRIANGLES) self._group.unset_state_recursive() - def __del__(self): - try: - if self._vertex_list is not None: - self._vertex_list.delete() - except: - pass - if _is_pyglet_doc_run: def on_animation_end(self): """The sprite animation reached the final frame. @@ -869,3 +798,46 @@ class Sprite(event.EventDispatcher): Sprite.register_event_type('on_animation_end') + + +class AdvancedSprite(pyglet.sprite.Sprite): + """Is a sprite that lets you change the shader program during initialization and after + For advanced users who understand shaders.""" + def __init__(self, + img, x=0, y=0, z=0, + blend_src=GL_SRC_ALPHA, + blend_dest=GL_ONE_MINUS_SRC_ALPHA, + batch=None, + group=None, + subpixel=False, + program=None): + + self._program = program + + if not program: + if isinstance(img, image.TextureArrayRegion): + self._program = get_default_array_shader() + else: + self._program = get_default_shader() + + super().__init__(img, x, y, z, blend_src, blend_dest, batch, group, subpixel) + + @property + def program(self): + return self._program + + @program.setter + def program(self, program): + if self._program == program: + return + self._group = self.group_class(self._texture, + self._group.blend_src, + self._group.blend_dest, + program, + self._group) + self._batch.migrate(self._vertex_list, GL_TRIANGLES, self._group, self._batch) + self._program = program + + + + diff --git a/libs/pyglet_rs/src/src/pymath.rs b/libs/pyglet_rs/src/src/pymath.rs index a7c3b19..fcfd682 100644 --- a/libs/pyglet_rs/src/src/pymath.rs +++ b/libs/pyglet_rs/src/src/pymath.rs @@ -9,6 +9,7 @@ pub mod python_class { use pyo3::class::basic::CompareOp; use pyo3::prelude::*; + use pyo3::types::PySlice; use crate::math::matrix::{Matrix3, Matrix4}; use crate::math::vector::{Vector2, Vector3, Vector4, VectorTrait}; @@ -131,6 +132,13 @@ pub mod python_class { return format!("Vector2_rs({}, {})", self.data.x, self.data.y); } + // fn __getitem__(&self, item: &PyAny) -> PyResult<&PyAny> { + // if item.is_instance_of::().unwrap() { + // let item = item.extract::().unwrap(); + // let indices = item.indices().unwrap(); + // } + // } + // getter and setter #[getter]