From 78e2f022dc4db4d533964c28696834a0f0ac1b7d Mon Sep 17 00:00:00 2001 From: Paul V Craven Date: Thu, 8 Oct 2026 08:57:35 -0500 Subject: [PATCH 1/2] Make mypy and pyright fail CI, and fix their errors Fixing the type errors turned up three crashes: - Window subclasses that stored a camera as self.camera (platformer tutorial steps 7-19, custom_sprite, the lights tutorial) raised AttributeError on startup. Window.camera is pyglet's default camera. They now use self.world_camera, and assigning Window.camera explains. - pixelated_ui() used pyglet 2 font filter attributes. - InputManager.parse() called a function renamed in #2289. A file-wide type: ignore had hidden it. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/code_quality.yml | 3 --- CHANGELOG.md | 4 +++ arcade/application.py | 22 +++++++++++---- arcade/cli/cli.py | 3 ++- arcade/context.py | 2 +- arcade/examples/easing_example_1.py | 13 ++++----- arcade/examples/easing_example_2.py | 11 ++++---- arcade/examples/gl/custom_sprite.py | 8 +++--- arcade/examples/gui/exp_animations_2.py | 11 ++++---- .../examples/platform_tutorial/07_camera.py | 8 +++--- arcade/examples/platform_tutorial/08_coins.py | 8 +++--- arcade/examples/platform_tutorial/09_sound.py | 8 +++--- arcade/examples/platform_tutorial/10_score.py | 8 +++--- arcade/examples/platform_tutorial/11_scene.py | 8 +++--- arcade/examples/platform_tutorial/12_tiled.py | 8 +++--- .../platform_tutorial/13_more_layers.py | 8 +++--- .../platform_tutorial/14_multiple_levels.py | 8 +++--- .../15_ladders_moving_platforms.py | 8 +++--- .../platform_tutorial/16_better_input.py | 8 +++--- .../platform_tutorial/17_animations.py | 8 +++--- .../examples/platform_tutorial/18_enemies.py | 8 +++--- .../examples/platform_tutorial/19_shooting.py | 8 +++--- arcade/examples/sprite_bullets_sweep.py | 4 +-- arcade/experimental/perspective_parallax.py | 8 +++--- arcade/gui/experimental/__init__.py | 5 ++-- arcade/gui/experimental/group.py | 3 ++- arcade/gui/widgets/dropdown.py | 4 +-- arcade/hitbox/__init__.py | 1 + arcade/input/__init__.py | 1 - arcade/input/input_mapping.py | 1 - arcade/input/manager.py | 14 +++++----- doc/tutorials/lights/01_light_demo.py | 18 ++++++------- doc/tutorials/lights/light_demo.py | 20 +++++++------- doc/tutorials/platform_tutorial/step_07.rst | 8 +++--- pyproject.toml | 5 ++++ tests/unit/gui/test_pixelated_ui.py | 19 +++++++++++++ tests/unit/test_input_manager.py | 27 ++++++++++++++++++- tests/unit/window/test_window.py | 7 +++++ 38 files changed, 197 insertions(+), 129 deletions(-) create mode 100644 tests/unit/gui/test_pixelated_ui.py diff --git a/.github/workflows/code_quality.yml b/.github/workflows/code_quality.yml index 4174745e14..b515a767e2 100644 --- a/.github/workflows/code_quality.yml +++ b/.github/workflows/code_quality.yml @@ -40,15 +40,12 @@ jobs: if: ${{ !cancelled() }} run: uv run make.py ruff-check - # Type checking doesn't fail the check yet: there are existing errors - name: Type Checking (MyPy) if: ${{ !cancelled() }} - continue-on-error: true run: uv run make.py mypy - name: Type Checking (Pyright) if: ${{ !cancelled() }} - continue-on-error: true run: uv run make.py pyright # This is a second job instead of an extra step because it takes the longest diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c69be30a9..f1f8b288b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,9 @@ Arcade [PyPi Release History](https://pypi.org/project/arcade/#history) page. - Fixed collision checks with `CollisionMethod.GPU` (also chosen automatically for lists of over 1500 sprites without a spatial hash) missing hit boxes that reach beyond a sprite's drawn size, such as a melee attack's reach. The GPU only knows the sprites' drawn sizes, so it now also uses the checking sprite's hit box, and each `SpriteList` keeps track of how far its sprites' hit boxes reach beyond their sizes. - Fixed drawing inside `TextureAtlas.render_into()` when the atlas had to rebuild to make room, for example to add a new texture being drawn. A full atlas with freed space rebuilds instead of growing, which moved the texture being drawn into, so the drawing landed on other textures and what was drawn before was lost. While rendering into it, the atlas now grows instead if it can, and a rebuild keeps what was drawn and moves the drawing area with the texture. - Fixed a `SpriteList` on WebGL only drawing its first 256 sprites when its storage was created with a capacity over 256: for example `SpriteList(capacity=1000)`, `clear()` on a list that had grown past 256 sprites, or a lazy list with more than 256 sprites before it was first drawn ([#2909](https://github.com/pythonarcade/arcade/issues/2909)). +- Fixed platformer tutorial steps 7 to 19, the `custom_sprite` example, and the lights tutorial crashing on startup with `AttributeError: property 'camera' ... has no setter`. They stored their camera as `self.camera` on the window, which is the default camera pyglet draws with. Their camera is now `self.world_camera`, and assigning `Window.camera` gives an error saying to use another name. +- Fixed `arcade.gui.experimental.pixelated_ui()` raising `AttributeError`. It now turns off text anti-aliasing and makes font glyphs use nearest-neighbor filtering, as documented. +- Fixed `InputManager.parse()` raising `AttributeError`, so a saved input configuration couldn't be loaded. ### New Features - Added `arcade.sweep_sprite(sprite, dx, dy, sprite_list)`, which checks the whole path of a moving sprite and returns a `SweepInfo` for the first sprite it would hit (the `sprite`, how far along the move as a `fraction` and `distance`, and the surface `normal`), or `None`. Fast sprites can't pass through thin walls this way. A sprite that already overlaps one is an immediate hit. Added the `sprite_bullets_sweep` example comparing it with a plain collision check. @@ -43,6 +46,7 @@ Arcade [PyPi Release History](https://pypi.org/project/arcade/#history) page. - Tile maps no longer use Pillow's `Image.getdata()`, which is deprecated and will be removed in Pillow 14, to apply an image layer's transparent color. It's done with Pillow operations instead, which is also faster (a 1024x600 image went from 76 ms to 4 ms). - Sped up removing many sprites from a large `SpriteList`, such as every bullet that hit something this frame. `SpriteList.remove()`, and so `Sprite.remove_from_sprite_lists()` and `kill()`, used to search the whole list for each sprite. Removals now wait until the list is next used, and many are applied in one pass: removing 100 sprites from 41,000 went from 19 ms to 3.3 ms, and 500 from 166,000 went from 630 ms to 17 ms. Drawing order is kept, and a few removals cost the same as before. - Documented choosing a spatial hash cell size (`spatial_hash_cell_size`) in the performance tips and the `SpriteList` docs: about the size of the sprites in the list. With 4 pixel sprites, collision checks were 16 times faster with 8 pixel cells than with the default of 128. +- Type checking with mypy and pyright now fails the CI check, like formatting and linting. Their existing errors are fixed. ## 4.0.0.dev8 diff --git a/arcade/application.py b/arcade/application.py index 5cd102bd6a..349801077a 100644 --- a/arcade/application.py +++ b/arcade/application.py @@ -18,9 +18,9 @@ if is_pyodide: pyglet.options.backend = "webgl" -import pyglet.config +import pyglet.config.gl import pyglet.window.mouse -from pyglet.config import GraphicsAPI +from pyglet.enums import GraphicsAPI from pyglet.display.base import Screen, ScreenMode from pyglet.event import EVENT_HANDLE_STATE, EVENT_UNHANDLED from pyglet.window import MouseCursor @@ -223,7 +223,7 @@ def __init__( graphics_api = _GL_API_MAP[gl_api] if antialiasing: try: - config = pyglet.config.OpenGLUserConfig( + config = pyglet.config.gl.OpenGLUserConfig( major_version=gl_version[0], minor_version=gl_version[1], api=graphics_api, @@ -243,7 +243,7 @@ def __init__( antialiasing = False # If we still don't have a config if not config: - config = pyglet.config.OpenGLUserConfig( + config = pyglet.config.gl.OpenGLUserConfig( major_version=gl_version[0], minor_version=gl_version[1], api=graphics_api, @@ -1099,7 +1099,9 @@ def default_camera(self) -> DefaultProjector: """ return self._ctx._default_camera - @property + # pyglet types this as one of its own cameras, but it needs to be arcade's + # DefaultProjector, so the override doesn't match + @property # type: ignore[override] def camera(self) -> DefaultProjector: """ Alias for :py:attr:`default_camera`. @@ -1109,9 +1111,19 @@ def camera(self) -> DefaultProjector: set, so this needs to resolve to the same :py:class:`DefaultProjector` to keep pyglet's internal drawing (e.g. :py:class:`~pyglet.text.Label`) going through arcade's projection/scissor handling. + + It can't be assigned. To keep your own camera on a window subclass, + give it another name, such as ``self.world_camera``. """ return self._ctx._default_camera + @camera.setter + def camera(self, value: object) -> None: + raise AttributeError( + "Window.camera is the default camera pyglet draws with, and can't be assigned. " + "Store your camera under another name, such as self.world_camera." + ) + @property def current_camera(self) -> Projector: """ diff --git a/arcade/cli/cli.py b/arcade/cli/cli.py index 7b80d8204c..11e682e2d0 100644 --- a/arcade/cli/cli.py +++ b/arcade/cli/cli.py @@ -12,7 +12,8 @@ def __init__(self): self.description: str = "Arcade Game Library CLI" def register_command(self, command_class: Type[BaseCommand]) -> None: - command = command_class() # type: ignore BaseCommand has different constructor than it's implementations + # BaseCommand has a different constructor than its implementations + command = command_class() # type: ignore[call-arg] self.commands[command.name] = command def create_parser(self) -> argparse.ArgumentParser: diff --git a/arcade/context.py b/arcade/context.py index c088fb5a68..922ee2b4ef 100644 --- a/arcade/context.py +++ b/arcade/context.py @@ -345,7 +345,7 @@ def reset(self) -> None: self.projection_matrix = Mat4.orthogonal_projection( 0, self.window.width, 0, self.window.height, -100, 100 ) - self._default_camera: DefaultProjector = DefaultProjector(context=self) + self._default_camera = DefaultProjector(context=self) self.current_camera = self._default_camera self.enable_only(self.BLEND) self.blend_func = self.BLEND_DEFAULT diff --git a/arcade/examples/easing_example_1.py b/arcade/examples/easing_example_1.py index 848cbf6dc7..0217fa3dfb 100644 --- a/arcade/examples/easing_example_1.py +++ b/arcade/examples/easing_example_1.py @@ -11,14 +11,15 @@ import arcade from arcade.anim import ease, Easing +from arcade.anim.easing import EasingFunction # --- Constants --- WINDOW_WIDTH = 1280 WINDOW_HEIGHT = 720 WINDOW_TITLE = "Easing Example 1" -X_START = 40 -X_END = 1200 +X_START = 40.0 +X_END = 1200.0 Y_INTERVAL = 60 BALL_RADIUS = 13 LINE_WIDTH = 1.0 @@ -48,8 +49,8 @@ class EasingCircle(arcade.SpriteCircle): """A ball that eases along the x-axis using a specific curve.""" - def __init__(self, radius: int, color: arcade.types.RGBOrA255, - ease_function: Easing): + def __init__(self, radius: int, color: arcade.types.Color, + ease_function: EasingFunction): super().__init__(radius, color) self.ease_function = ease_function self.start_time = 0.0 @@ -61,12 +62,12 @@ class GameView(arcade.View): def __init__(self): super().__init__() self.background_color = BACKGROUND_COLOR - self.ball_list: arcade.SpriteList[EasingCircle] | None = None + self.ball_list: arcade.SpriteList[EasingCircle] = arcade.SpriteList() self.time_elapsed = 0.0 def setup(self): """Create one ball per easing function.""" - self.ball_list = arcade.SpriteList() + self.ball_list.clear() self.time_elapsed = 0.0 for index, (label, ease_func) in enumerate(EASING_LIST): diff --git a/arcade/examples/easing_example_2.py b/arcade/examples/easing_example_2.py index 2e73737a32..c71c788422 100644 --- a/arcade/examples/easing_example_2.py +++ b/arcade/examples/easing_example_2.py @@ -68,7 +68,10 @@ def __init__(self): super().__init__() self.background_color = arcade.color.BLACK - self.ship_sprite: arcade.Sprite | None = None + self.ship_sprite = arcade.Sprite( + ":resources:images/space_shooter/playerShip1_orange.png", + scale=0.5, + ) self.mode = 1 self.time_elapsed = 0.0 @@ -90,10 +93,6 @@ def __init__(self): def setup(self): """Set up the game.""" - self.ship_sprite = arcade.Sprite( - ":resources:images/space_shooter/playerShip1_orange.png", - scale=0.5, - ) self.ship_sprite.center_x = WINDOW_WIDTH / 2 self.ship_sprite.center_y = WINDOW_HEIGHT / 2 self.time_elapsed = 0.0 @@ -154,7 +153,7 @@ def _start_position_ease(self): def on_update(self, delta_time: float): """Update ship angle and/or position based on current mode.""" self.time_elapsed += delta_time - ease_func = MODE_EASING.get(self.mode) + ease_func = MODE_EASING.get(self.mode, Easing.LINEAR) if self.mode == 1: # Instant angle — always face the target directly diff --git a/arcade/examples/gl/custom_sprite.py b/arcade/examples/gl/custom_sprite.py index 2718be7b07..5c54faacdd 100644 --- a/arcade/examples/gl/custom_sprite.py +++ b/arcade/examples/gl/custom_sprite.py @@ -34,7 +34,7 @@ class GeoSprites(arcade.Window): def __init__(self): super().__init__(800, 600, "Custom Sprites", resizable=True) - self.camera = arcade.Camera2D() + self.world_camera = arcade.Camera2D() self.program = self.ctx.program( vertex_shader=""" #version 330 @@ -150,11 +150,11 @@ def __init__(self): def on_resize(self, width: int, height: int): super().on_resize(width, height) - self.camera.match_window(position=True) + self.world_camera.match_window(position=True) def on_draw(self): self.clear() - self.camera.use() + self.world_camera.use() # Bind our sprite texture to channel 0 self.texture.use(unit=0) # Render the sprite data with our shader @@ -162,7 +162,7 @@ def on_draw(self): def on_mouse_drag(self, x: float, y: float, dx: float, dy: float, buttons: int, modifiers: int): """Make it easier to explore the geometry by scrolling""" - self.camera.position = self.camera.position[0] - dx, self.camera.position[1] - dy + self.world_camera.position = self.world_camera.position[0] - dx, self.world_camera.position[1] - dy def gen_sprites(self, count: int): """Quickly generate some random sprite data""" diff --git a/arcade/examples/gui/exp_animations_2.py b/arcade/examples/gui/exp_animations_2.py index 010c12928b..a24331bd56 100644 --- a/arcade/examples/gui/exp_animations_2.py +++ b/arcade/examples/gui/exp_animations_2.py @@ -92,12 +92,11 @@ def main(): if __name__ == "__main__": - import pyglet - # pyglet.options.text_antialiasing = False - # - from pyglet.graphics.api.gl import GL_NEAREST - pyglet.font.base.Font.texture_min_filter = GL_NEAREST - pyglet.font.base.Font.texture_mag_filter = GL_NEAREST + import pyglet.font.base + from pyglet.enums import TextureFilter + + # Keep the pixel font sharp when it's scaled + pyglet.font.base.Font.filters = TextureFilter.NEAREST resources.load_kenney_fonts() main() diff --git a/arcade/examples/platform_tutorial/07_camera.py b/arcade/examples/platform_tutorial/07_camera.py index 002862eeaf..2a49fa0564 100644 --- a/arcade/examples/platform_tutorial/07_camera.py +++ b/arcade/examples/platform_tutorial/07_camera.py @@ -47,7 +47,7 @@ def __init__(self): self.wall_list = None # A variable to store our camera object - self.camera = None + self.world_camera = None def setup(self): """Set up the game here. Call this function to restart the game.""" @@ -96,7 +96,7 @@ def setup(self): ) # Initialize our camera, setting a viewport the size of our window. - self.camera = arcade.Camera2D() + self.world_camera = arcade.Camera2D() self.background_color = arcade.csscolor.CORNFLOWER_BLUE @@ -107,7 +107,7 @@ def on_draw(self): self.clear() # Activate our camera before drawing - self.camera.use() + self.world_camera.use() # Draw our sprites self.player_list.draw() @@ -120,7 +120,7 @@ def on_update(self, delta_time): self.physics_engine.update() # Center our camera on the player - self.camera.position = self.player_sprite.position + self.world_camera.position = self.player_sprite.position def on_key_press(self, key, modifiers): """Called whenever a key is pressed.""" diff --git a/arcade/examples/platform_tutorial/08_coins.py b/arcade/examples/platform_tutorial/08_coins.py index dfe0fdf260..c313b8b98d 100644 --- a/arcade/examples/platform_tutorial/08_coins.py +++ b/arcade/examples/platform_tutorial/08_coins.py @@ -51,7 +51,7 @@ def __init__(self): self.coin_list = None # A variable to store our camera object - self.camera = None + self.world_camera = None def setup(self): """Set up the game here. Call this function to restart the game.""" @@ -108,7 +108,7 @@ def setup(self): ) # Initialize our camera, setting a viewport the size of our window. - self.camera = arcade.Camera2D() + self.world_camera = arcade.Camera2D() self.background_color = arcade.csscolor.CORNFLOWER_BLUE @@ -119,7 +119,7 @@ def on_draw(self): self.clear() # Activate our camera before drawing - self.camera.use() + self.world_camera.use() # Draw our sprites self.player_list.draw() @@ -143,7 +143,7 @@ def on_update(self, delta_time): coin.remove_from_sprite_lists() # Center our camera on the player - self.camera.position = self.player_sprite.position + self.world_camera.position = self.player_sprite.position def on_key_press(self, key, modifiers): """Called whenever a key is pressed.""" diff --git a/arcade/examples/platform_tutorial/09_sound.py b/arcade/examples/platform_tutorial/09_sound.py index 3524b4cb94..873728d74e 100644 --- a/arcade/examples/platform_tutorial/09_sound.py +++ b/arcade/examples/platform_tutorial/09_sound.py @@ -51,7 +51,7 @@ def __init__(self): self.coin_list = None # A variable to store our camera object - self.camera = None + self.world_camera = None # Load sounds self.collect_coin_sound = arcade.load_sound(":resources:sounds/coin1.wav") @@ -112,7 +112,7 @@ def setup(self): ) # Initialize our camera, setting a viewport the size of our window. - self.camera = arcade.Camera2D() + self.world_camera = arcade.Camera2D() self.background_color = arcade.csscolor.CORNFLOWER_BLUE @@ -123,7 +123,7 @@ def on_draw(self): self.clear() # Activate our camera before drawing - self.camera.use() + self.world_camera.use() # Draw our sprites self.player_list.draw() @@ -148,7 +148,7 @@ def on_update(self, delta_time): arcade.play_sound(self.collect_coin_sound) # Center our camera on the player - self.camera.position = self.player_sprite.position + self.world_camera.position = self.player_sprite.position def on_key_press(self, key, modifiers): """Called whenever a key is pressed.""" diff --git a/arcade/examples/platform_tutorial/10_score.py b/arcade/examples/platform_tutorial/10_score.py index d183abc1c8..763581fc31 100644 --- a/arcade/examples/platform_tutorial/10_score.py +++ b/arcade/examples/platform_tutorial/10_score.py @@ -51,7 +51,7 @@ def __init__(self): self.coin_list = None # A variable to store our camera object - self.camera = None + self.world_camera = None # A variable to store our gui camera object self.gui_camera = None @@ -121,7 +121,7 @@ def setup(self): ) # Initialize our camera, setting a viewport the size of our window. - self.camera = arcade.Camera2D() + self.world_camera = arcade.Camera2D() # Initialize our gui camera, initial settings are the same as our world camera. self.gui_camera = arcade.Camera2D() @@ -141,7 +141,7 @@ def on_draw(self): self.clear() # Activate our camera before drawing - self.camera.use() + self.world_camera.use() # Draw our sprites self.player_list.draw() @@ -174,7 +174,7 @@ def on_update(self, delta_time): self.score_text.text = f"Score: {self.score}" # Center our camera on the player - self.camera.position = self.player_sprite.position + self.world_camera.position = self.player_sprite.position def on_key_press(self, key, modifiers): """Called whenever a key is pressed.""" diff --git a/arcade/examples/platform_tutorial/11_scene.py b/arcade/examples/platform_tutorial/11_scene.py index ae22ee5023..ae8830677b 100644 --- a/arcade/examples/platform_tutorial/11_scene.py +++ b/arcade/examples/platform_tutorial/11_scene.py @@ -40,7 +40,7 @@ def __init__(self): self.scene = None # A variable to store our camera object - self.camera = None + self.world_camera = None # A variable to store our gui camera object self.gui_camera = None @@ -110,7 +110,7 @@ def setup(self): ) # Initialize our camera, setting a viewport the size of our window. - self.camera = arcade.Camera2D() + self.world_camera = arcade.Camera2D() # Initialize our gui camera, initial settings are the same as our world camera. self.gui_camera = arcade.Camera2D() @@ -130,7 +130,7 @@ def on_draw(self): self.clear() # Activate our camera before drawing - self.camera.use() + self.world_camera.use() # Draw our Scene self.scene.draw() @@ -161,7 +161,7 @@ def on_update(self, delta_time): self.score_text.text = f"Score: {self.score}" # Center our camera on the player - self.camera.position = self.player_sprite.position + self.world_camera.position = self.player_sprite.position def on_key_press(self, key, modifiers): """Called whenever a key is pressed.""" diff --git a/arcade/examples/platform_tutorial/12_tiled.py b/arcade/examples/platform_tutorial/12_tiled.py index 995358957a..3661a1d354 100644 --- a/arcade/examples/platform_tutorial/12_tiled.py +++ b/arcade/examples/platform_tutorial/12_tiled.py @@ -43,7 +43,7 @@ def __init__(self): self.scene = None # A variable to store our camera object - self.camera = None + self.world_camera = None # A variable to store our gui camera object self.gui_camera = None @@ -97,7 +97,7 @@ def setup(self): ) # Initialize our camera, setting a viewport the size of our window. - self.camera = arcade.Camera2D() + self.world_camera = arcade.Camera2D() # Initialize our gui camera, initial settings are the same as our world camera. self.gui_camera = arcade.Camera2D() @@ -117,7 +117,7 @@ def on_draw(self): self.clear() # Activate our camera before drawing - self.camera.use() + self.world_camera.use() # Draw our Scene self.scene.draw() @@ -148,7 +148,7 @@ def on_update(self, delta_time): self.score_text.text = f"Score: {self.score}" # Center our camera on the player - self.camera.position = self.player_sprite.position + self.world_camera.position = self.player_sprite.position def on_key_press(self, key, modifiers): """Called whenever a key is pressed.""" diff --git a/arcade/examples/platform_tutorial/13_more_layers.py b/arcade/examples/platform_tutorial/13_more_layers.py index c3fe1298b4..88b5e6282a 100644 --- a/arcade/examples/platform_tutorial/13_more_layers.py +++ b/arcade/examples/platform_tutorial/13_more_layers.py @@ -43,7 +43,7 @@ def __init__(self): self.scene = None # A variable to store our camera object - self.camera = None + self.world_camera = None # A variable to store our gui camera object self.gui_camera = None @@ -105,7 +105,7 @@ def setup(self): ) # Initialize our camera, setting a viewport the size of our window. - self.camera = arcade.Camera2D() + self.world_camera = arcade.Camera2D() # Initialize our gui camera, initial settings are the same as our world camera. self.gui_camera = arcade.Camera2D() @@ -125,7 +125,7 @@ def on_draw(self): self.clear() # Activate our camera before drawing - self.camera.use() + self.world_camera.use() # Draw our Scene self.scene.draw() @@ -162,7 +162,7 @@ def on_update(self, delta_time): self.setup() # Center our camera on the player - self.camera.position = self.player_sprite.position + self.world_camera.position = self.player_sprite.position def on_key_press(self, key, modifiers): """Called whenever a key is pressed.""" diff --git a/arcade/examples/platform_tutorial/14_multiple_levels.py b/arcade/examples/platform_tutorial/14_multiple_levels.py index 9daeb96426..0499230868 100644 --- a/arcade/examples/platform_tutorial/14_multiple_levels.py +++ b/arcade/examples/platform_tutorial/14_multiple_levels.py @@ -43,7 +43,7 @@ def __init__(self): self.scene = None # A variable to store our camera object - self.camera = None + self.world_camera = None # A variable to store our gui camera object self.gui_camera = None @@ -114,7 +114,7 @@ def setup(self): ) # Initialize our camera, setting a viewport the size of our window. - self.camera = arcade.Camera2D() + self.world_camera = arcade.Camera2D() # Initialize our gui camera, initial settings are the same as our world camera. self.gui_camera = arcade.Camera2D() @@ -141,7 +141,7 @@ def on_draw(self): self.clear() # Activate our camera before drawing - self.camera.use() + self.world_camera.use() # Draw our Scene self.scene.draw() @@ -189,7 +189,7 @@ def on_update(self, delta_time): self.setup() # Center our camera on the player - self.camera.position = self.player_sprite.position + self.world_camera.position = self.player_sprite.position def on_key_press(self, key, modifiers): """Called whenever a key is pressed.""" diff --git a/arcade/examples/platform_tutorial/15_ladders_moving_platforms.py b/arcade/examples/platform_tutorial/15_ladders_moving_platforms.py index d0a0a9c106..bd0384d4f3 100644 --- a/arcade/examples/platform_tutorial/15_ladders_moving_platforms.py +++ b/arcade/examples/platform_tutorial/15_ladders_moving_platforms.py @@ -43,7 +43,7 @@ def __init__(self): self.scene = None # A variable to store our camera object - self.camera = None + self.world_camera = None # A variable to store our gui camera object self.gui_camera = None @@ -114,7 +114,7 @@ def setup(self): ) # Initialize our camera, setting a viewport the size of our window. - self.camera = arcade.Camera2D() + self.world_camera = arcade.Camera2D() # Initialize our gui camera, initial settings are the same as our world camera. self.gui_camera = arcade.Camera2D() @@ -140,7 +140,7 @@ def on_draw(self): self.clear() # Activate our camera before drawing - self.camera.use() + self.world_camera.use() # Draw our Scene self.scene.draw() @@ -171,7 +171,7 @@ def on_update(self, delta_time): self.score_text.text = f"Score: {self.score}" # Center our camera on the player - self.camera.position = self.player_sprite.position + self.world_camera.position = self.player_sprite.position def on_key_press(self, key, modifiers): """Called whenever a key is pressed.""" diff --git a/arcade/examples/platform_tutorial/16_better_input.py b/arcade/examples/platform_tutorial/16_better_input.py index 714f1d1e1a..8672f6962a 100644 --- a/arcade/examples/platform_tutorial/16_better_input.py +++ b/arcade/examples/platform_tutorial/16_better_input.py @@ -49,7 +49,7 @@ def __init__(self): self.scene = None # A variable to store our camera object - self.camera = None + self.world_camera = None # A variable to store our gui camera object self.gui_camera = None @@ -120,7 +120,7 @@ def setup(self): ) # Initialize our camera, setting a viewport the size of our window. - self.camera = arcade.Camera2D() + self.world_camera = arcade.Camera2D() # Initialize our gui camera, initial settings are the same as our world camera. self.gui_camera = arcade.Camera2D() @@ -146,7 +146,7 @@ def on_draw(self): self.clear() # Activate our camera before drawing - self.camera.use() + self.world_camera.use() # Draw our Scene self.scene.draw() @@ -177,7 +177,7 @@ def on_update(self, delta_time): self.score_text.text = f"Score: {self.score}" # Center our camera on the player - self.camera.position = self.player_sprite.position + self.world_camera.position = self.player_sprite.position def process_keychange(self): # First handle the case where we have moved up. This needs to be handled diff --git a/arcade/examples/platform_tutorial/17_animations.py b/arcade/examples/platform_tutorial/17_animations.py index b6f36e65ff..eabf9a2ef5 100644 --- a/arcade/examples/platform_tutorial/17_animations.py +++ b/arcade/examples/platform_tutorial/17_animations.py @@ -133,7 +133,7 @@ def __init__(self): self.scene = None # A variable to store our camera object - self.camera = None + self.world_camera = None # A variable to store our gui camera object self.gui_camera = None @@ -200,7 +200,7 @@ def setup(self): ) # Initialize our camera, setting a viewport the size of our window. - self.camera = arcade.Camera2D() + self.world_camera = arcade.Camera2D() # Initialize our gui camera, initial settings are the same as our world camera. self.gui_camera = arcade.Camera2D() @@ -226,7 +226,7 @@ def on_draw(self): self.clear() # Activate our camera before drawing - self.camera.use() + self.world_camera.use() # Draw our Scene self.scene.draw() @@ -275,7 +275,7 @@ def on_update(self, delta_time): self.score_text.text = f"Score: {self.score}" # Center our camera on the player - self.camera.position = self.player_sprite.position + self.world_camera.position = self.player_sprite.position def process_keychange(self): # First handle the case where we have moved up. This needs to be handled diff --git a/arcade/examples/platform_tutorial/18_enemies.py b/arcade/examples/platform_tutorial/18_enemies.py index 148d38d24b..0f2cd6ccf2 100644 --- a/arcade/examples/platform_tutorial/18_enemies.py +++ b/arcade/examples/platform_tutorial/18_enemies.py @@ -183,7 +183,7 @@ def __init__(self): self.scene = None # A variable to store our camera object - self.camera = None + self.world_camera = None # A variable to store our gui camera object self.gui_camera = None @@ -277,7 +277,7 @@ def setup(self): ) # Initialize our camera, setting a viewport the size of our window. - self.camera = arcade.Camera2D() + self.world_camera = arcade.Camera2D() # Initialize our gui camera, initial settings are the same as our world camera. self.gui_camera = arcade.Camera2D() @@ -303,7 +303,7 @@ def on_draw(self): self.clear() # Activate our camera before drawing - self.camera.use() + self.world_camera.use() # Draw our Scene self.scene.draw() @@ -369,7 +369,7 @@ def on_update(self, delta_time): self.score_text.text = f"Score: {self.score}" # Center our camera on the player - self.camera.position = self.player_sprite.position + self.world_camera.position = self.player_sprite.position def process_keychange(self): # First handle the case where we have moved up. This needs to be handled diff --git a/arcade/examples/platform_tutorial/19_shooting.py b/arcade/examples/platform_tutorial/19_shooting.py index 428d962778..433f3a00a5 100644 --- a/arcade/examples/platform_tutorial/19_shooting.py +++ b/arcade/examples/platform_tutorial/19_shooting.py @@ -184,7 +184,7 @@ def __init__(self): self.scene = None # A variable to store our camera object - self.camera = None + self.world_camera = None # A variable to store our gui camera object self.gui_camera = None @@ -284,7 +284,7 @@ def setup(self): ) # Initialize our camera, setting a viewport the size of our window. - self.camera = arcade.Camera2D() + self.world_camera = arcade.Camera2D() # Initialize our gui camera, initial settings are the same as our world camera. self.gui_camera = arcade.Camera2D() @@ -317,7 +317,7 @@ def on_draw(self): self.clear() # Activate our camera before drawing - self.camera.use() + self.world_camera.use() # Draw our Scene self.scene.draw() @@ -437,7 +437,7 @@ def on_update(self, delta_time): self.score_text.text = f"Score: {self.score}" # Center our camera on the player - self.camera.position = self.player_sprite.position + self.world_camera.position = self.player_sprite.position def process_keychange(self): # First handle the case where we have moved up. This needs to be handled diff --git a/arcade/examples/sprite_bullets_sweep.py b/arcade/examples/sprite_bullets_sweep.py index d347b21ff5..dbbfd69d84 100644 --- a/arcade/examples/sprite_bullets_sweep.py +++ b/arcade/examples/sprite_bullets_sweep.py @@ -49,7 +49,7 @@ def __init__(self, bottom: int, top: int, use_sweep: bool): self.hits = 0 self.passed = 0 - self.walls = arcade.SpriteList() + self.walls: arcade.SpriteList[arcade.SpriteSolidColor] = arcade.SpriteList() for x in WALL_X_POSITIONS: wall = arcade.SpriteSolidColor( WALL_WIDTH, @@ -60,7 +60,7 @@ def __init__(self, bottom: int, top: int, use_sweep: bool): ) self.walls.append(wall) - self.lasers = arcade.SpriteList() + self.lasers: arcade.SpriteList[arcade.Sprite] = arcade.SpriteList() def fire(self): """Add a laser at the left edge, at a random height.""" diff --git a/arcade/experimental/perspective_parallax.py b/arcade/experimental/perspective_parallax.py index 864ad733ec..9ab96b1d41 100644 --- a/arcade/experimental/perspective_parallax.py +++ b/arcade/experimental/perspective_parallax.py @@ -23,12 +23,12 @@ class PerspectiveParallax(arcade.Window): def __init__(self): super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, "Perspective Parallax") self.t = 0.0 - self.camera = arcade.camera.PerspectiveProjector() + self.projector = arcade.camera.PerspectiveProjector() - self.camera_data = self.camera.view + self.camera_data = self.projector.view self.camera_data.zoom = 2.0 - self.camera.projection.far = 1000 + self.projector.projection.far = 1000 self.background_sprites = arcade.SpriteList() for index, layer_src in enumerate(LAYERS): @@ -38,7 +38,7 @@ def __init__(self): def on_draw(self): self.clear() - with self.camera.activate(): + with self.projector.activate(): self.background_sprites.draw(pixelated=True) def on_update(self, delta_time: float): diff --git a/arcade/gui/experimental/__init__.py b/arcade/gui/experimental/__init__.py index 0d6b10f125..1d89887b02 100644 --- a/arcade/gui/experimental/__init__.py +++ b/arcade/gui/experimental/__init__.py @@ -34,10 +34,11 @@ def pixelated_ui(): (Best Practices: within main function, before creating the window.) """ import pyglet + import pyglet.font.base + from pyglet.enums import TextureFilter pyglet.options.text_antialiasing = False - pyglet.font.base.Font.texture_min_filter = pyglet.gl.GL_NEAREST - pyglet.font.base.Font.texture_mag_filter = pyglet.gl.GL_NEAREST + pyglet.font.base.Font.filters = TextureFilter.NEAREST UIManager._pixelated = True diff --git a/arcade/gui/experimental/group.py b/arcade/gui/experimental/group.py index 7531a7c1d6..e8c1fb6c39 100644 --- a/arcade/gui/experimental/group.py +++ b/arcade/gui/experimental/group.py @@ -52,7 +52,8 @@ class UIRenderGroup(UILayout): Defaults to the center of the group.""" alpha = Property(255) """Alpha value (0-255) applied to the whole subtree.""" - scale = Property(1.0) # type: ignore # intentionally shadows UIWidget.scale() + # Intentionally shadows UIWidget.scale() + scale: Property[float | tuple[float, float]] = Property(1.0) # type: ignore[assignment] """Scale factor applied around ``anchor``. Shadows :meth:`UIWidget.scale`, which resizes the rect instead.""" tint = Property(WHITE) diff --git a/arcade/gui/widgets/dropdown.py b/arcade/gui/widgets/dropdown.py index 35cd3008a4..a6b5957157 100644 --- a/arcade/gui/widgets/dropdown.py +++ b/arcade/gui/widgets/dropdown.py @@ -10,7 +10,7 @@ from arcade.gui.experimental.focus import UIFocusMixin from arcade.gui.experimental.scroll_area import UIScrollBar from arcade.gui.ui_manager import UIManager -from arcade.gui.widgets import UILayout, UIWidget +from arcade.gui.widgets import UILayout, UIWidget, W from arcade.gui.widgets.buttons import UIFlatButton from arcade.gui.widgets.layout import UIBoxLayout @@ -56,7 +56,7 @@ def __init__( self._scroll_bar.rect = self._scroll_bar.rect.resize(width=self.SCROLL_BAR_WIDTH) super().add(self._scroll_bar) - def add_option(self, widget: UIWidget) -> UIWidget: + def add_option(self, widget: W) -> W: """Add an option widget to the options layout.""" return self._options_layout.add(widget) diff --git a/arcade/hitbox/__init__.py b/arcade/hitbox/__init__.py index b83789c4e9..ae71de9272 100644 --- a/arcade/hitbox/__init__.py +++ b/arcade/hitbox/__init__.py @@ -11,6 +11,7 @@ algo_simple = SimpleHitBoxAlgorithm() #: The detailed hit box algorithm. This depends on pymunk and will fallback to the simple algorithm. +algo_detailed: HitBoxAlgorithm try: from .pymunk import PymunkHitBoxAlgorithm diff --git a/arcade/input/__init__.py b/arcade/input/__init__.py index 8b4e086c8f..10eb543018 100644 --- a/arcade/input/__init__.py +++ b/arcade/input/__init__.py @@ -1,5 +1,4 @@ # ruff: noqa: F401 -# type: ignore from .inputs import ( ControllerButtons, diff --git a/arcade/input/input_mapping.py b/arcade/input/input_mapping.py index d030ad7ce6..ac3332ab41 100644 --- a/arcade/input/input_mapping.py +++ b/arcade/input/input_mapping.py @@ -1,4 +1,3 @@ -# type: ignore from __future__ import annotations from arcade.input import inputs diff --git a/arcade/input/manager.py b/arcade/input/manager.py index e8d35d300e..9767f8aebb 100644 --- a/arcade/input/manager.py +++ b/arcade/input/manager.py @@ -1,4 +1,3 @@ -# type: ignore from __future__ import annotations from collections.abc import Callable @@ -31,16 +30,15 @@ class RawInputManager(TypedDict): controller_deadzone: float -_K = TypeVar("_K") _V = TypeVar("_V") -def _clean_dicts(to_remove: _V, *dicts_to_clean: dict[_K, set[_V]]) -> None: +def _clean_dicts(to_remove: _V, *dicts_to_clean: dict[Any, set[_V]]) -> None: """Clean a dictionary in-place. This helps simplify serialization code for controller config. """ - to_discard = [] + to_discard: list[Any] = [] for to_clean in dicts_to_clean: if to_discard: to_discard.clear() @@ -202,7 +200,7 @@ def parse(cls, raw: RawInputManager) -> InputManager: final.new_action(name) for raw_mapping in raw_action["mappings"]: - input_instance = inputs.parse_instance(raw_mapping) + input_instance = inputs.parse_mapping_input_enum(raw_mapping) final.add_action_input( name, @@ -215,10 +213,10 @@ def parse(cls, raw: RawInputManager) -> InputManager: for raw_axis in raw["axes"]: name = raw_axis["name"] final.new_axis(name) - for raw_mapping in raw_axis["mappings"]: - input_instance = inputs.parse_instance(raw_mapping) + for raw_axis_mapping in raw_axis["mappings"]: + input_instance = inputs.parse_mapping_input_enum(raw_axis_mapping) - final.add_axis_input(name, input_instance, raw_mapping["scale"]) + final.add_axis_input(name, input_instance, raw_axis_mapping["scale"]) return final diff --git a/doc/tutorials/lights/01_light_demo.py b/doc/tutorials/lights/01_light_demo.py index 94bb598752..79409f5a92 100644 --- a/doc/tutorials/lights/01_light_demo.py +++ b/doc/tutorials/lights/01_light_demo.py @@ -26,7 +26,7 @@ def __init__(self, width, height, title): self.physics_engine = None # camera for scrolling - self.camera = None + self.world_camera = None def setup(self): """ Create everything """ @@ -52,7 +52,7 @@ def setup(self): self.physics_engine = arcade.PhysicsEngineSimple(self.player_sprite, self.wall_list) # setup camera - self.camera = arcade.Camera2D() + self.world_camera = arcade.Camera2D() def on_draw(self): """ Draw everything. """ @@ -83,10 +83,10 @@ def on_key_release(self, key, _): def scroll_screen(self): """ Manage Scrolling """ - position = self.camera.position + position = self.world_camera.position - top_left = self.camera.top_left - bottom_right = self.camera.bottom_right + top_left = self.world_camera.top_left + bottom_right = self.world_camera.bottom_right # Scroll left left_boundary = top_left[0] + VIEWPORT_MARGIN @@ -108,16 +108,16 @@ def scroll_screen(self): if self.player_sprite.bottom < bottom_boundary: position = position[0], position[1] + (self.player_sprite.bottom - bottom_boundary) - self.camera.position = position + self.world_camera.position = position # Make sure our boundaries are integer values. While the viewport does # support floating point numbers, for this application we want every pixel # in the view port to map directly onto a pixel on the screen. We don't want # any rounding errors. - bottom_left = self.camera.bottom_left - self.camera.bottom_left = int(bottom_left[0]), int(bottom_left[1]) + bottom_left = self.world_camera.bottom_left + self.world_camera.bottom_left = int(bottom_left[0]), int(bottom_left[1]) - self.camera.use() + self.world_camera.use() def on_update(self, delta_time): """ Movement and game logic """ diff --git a/doc/tutorials/lights/light_demo.py b/doc/tutorials/lights/light_demo.py index 6fb8feb01b..aaa6a0bdde 100644 --- a/doc/tutorials/lights/light_demo.py +++ b/doc/tutorials/lights/light_demo.py @@ -38,7 +38,7 @@ def __init__(self, width, height, title): self.physics_engine = None # camera for scrolling - self.camera = None + self.world_camera = None # --- Light related --- # List of all the lights @@ -186,7 +186,7 @@ def setup(self): self.physics_engine = arcade.PhysicsEngineSimple(self.player_sprite, self.wall_list) # setup camera - self.camera = arcade.Camera2D() + self.world_camera = arcade.Camera2D() def on_draw(self): """ Draw everything. """ @@ -206,7 +206,7 @@ def on_draw(self): self.light_layer.draw(ambient_color=AMBIENT_COLOR) # Now draw anything that should NOT be affected by lighting. - left, bottom = self.camera.bottom_left + left, bottom = self.world_camera.bottom_left arcade.draw_text("Press SPACE to turn character light on/off.", 10 + left, 10 + bottom, arcade.color.WHITE, 20) @@ -251,10 +251,10 @@ def on_key_release(self, key, _): def scroll_screen(self): """ Manage Scrolling """ - position = self.camera.position + position = self.world_camera.position - top_left = self.camera.top_left - bottom_right = self.camera.bottom_right + top_left = self.world_camera.top_left + bottom_right = self.world_camera.bottom_right # Scroll left left_boundary = top_left[0] + VIEWPORT_MARGIN @@ -276,16 +276,16 @@ def scroll_screen(self): if self.player_sprite.bottom < bottom_boundary: position = position[0], position[1] + (self.player_sprite.bottom - bottom_boundary) - self.camera.position = position + self.world_camera.position = position # Make sure our boundaries are integer values. While the viewport does # support floating point numbers, for this application we want every pixel # in the view port to map directly onto a pixel on the screen. We don't want # any rounding errors. - bottom_left = self.camera.bottom_left - self.camera.bottom_left = int(bottom_left[0]), int(bottom_left[1]) + bottom_left = self.world_camera.bottom_left + self.world_camera.bottom_left = int(bottom_left[0]), int(bottom_left[1]) - self.camera.use() + self.world_camera.use() def on_update(self, delta_time): """ Movement and game logic """ diff --git a/doc/tutorials/platform_tutorial/step_07.rst b/doc/tutorials/platform_tutorial/step_07.rst index 1d5b902ab5..7f802f98a5 100644 --- a/doc/tutorials/platform_tutorial/step_07.rst +++ b/doc/tutorials/platform_tutorial/step_07.rst @@ -14,13 +14,13 @@ To start with, let's go ahead and add a variable in our ``__init__`` function to .. code-block:: - self.camera = None + self.world_camera = None Next we can go to our setup function, and initialize it like so: .. code-block:: - self.camera = arcade.Camera2D() + self.world_camera = arcade.Camera2D() Since we're drawing to the entire screen, we can use :py:class:`~arcade.Camera2D`'s default settings. In other circumstances, we can create or adjust the camera so it has a different viewport. @@ -32,7 +32,7 @@ drawing our SpriteLists: .. code-block:: - self.camera.use() + self.world_camera.use() If you run the game at this point, you might notice that nothing has changed, our game is still one static un-moving screen. This is because we are never updating the camera's position. In our platformer game, we want the camera to follow @@ -47,7 +47,7 @@ consideration. .. code-block:: - self.camera.position = self.player_sprite.position + self.world_camera.position = self.player_sprite.position Source Code ~~~~~~~~~~~ diff --git a/pyproject.toml b/pyproject.toml index 05b2e45704..2470bb34f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -210,3 +210,8 @@ ignore_missing_imports = true [[tool.mypy.overrides]] module = "PyInstaller.*" ignore_missing_imports = true + +# Only the numpy_sprite example uses it, and it isn't a dependency +[[tool.mypy.overrides]] +module = "numpy.*" +ignore_missing_imports = true diff --git a/tests/unit/gui/test_pixelated_ui.py b/tests/unit/gui/test_pixelated_ui.py new file mode 100644 index 0000000000..82837243d5 --- /dev/null +++ b/tests/unit/gui/test_pixelated_ui.py @@ -0,0 +1,19 @@ +import pyglet +import pyglet.font.base +from pyglet.enums import TextureFilter + +from arcade.gui import UIManager +from arcade.gui.experimental import pixelated_ui + + +def test_pixelated_ui_sets_nearest_font_filtering(monkeypatch): + # Let monkeypatch put the global settings back afterwards + monkeypatch.setattr(pyglet.options, "text_antialiasing", True) + monkeypatch.setattr(pyglet.font.base.Font, "filters", TextureFilter.LINEAR) + monkeypatch.setattr(UIManager, "_pixelated", False) + + pixelated_ui() + + assert pyglet.options.text_antialiasing is False + assert pyglet.font.base.Font.filters == TextureFilter.NEAREST + assert UIManager._pixelated is True diff --git a/tests/unit/test_input_manager.py b/tests/unit/test_input_manager.py index fcf7e40619..7c2a9bd146 100644 --- a/tests/unit/test_input_manager.py +++ b/tests/unit/test_input_manager.py @@ -2,7 +2,7 @@ from pyglet.math import Vec2 -from arcade.input.inputs import ControllerButtons +from arcade.input.inputs import ControllerButtons, ControllerSticks, Keys from arcade.input.manager import ActionState, InputDevice, InputManager @@ -53,3 +53,28 @@ def test_dpad_motion_ignores_unchanged_directions(): manager.on_dpad_motion(Mock(), Vec2(0, -1)) dispatch_action.assert_not_called() + + +def test_parse_restores_serialized_actions_and_axes(window): + manager = InputManager() + manager.new_action("jump") + manager.add_action_input("jump", Keys.SPACE, mod_shift=True) + manager.add_action_input("jump", ControllerButtons.BOTTOM_FACE) + manager.new_axis("move") + manager.add_axis_input("move", Keys.LEFT, -1.0) + manager.add_axis_input("move", ControllerSticks.LEFT_STICK_X, 1.0) + raw = manager.serialize() + + parsed = InputManager.parse(raw) + + # Each action and axis keeps its mappings in a set, so ignore their order + def mappings(raw_manager): + return { + entry["name"]: sorted(map(repr, entry["mappings"])) + for entry in raw_manager["actions"] + raw_manager["axes"] + } + + assert mappings(parsed.serialize()) == mappings(raw) + assert parsed.controller_deadzone == manager.controller_deadzone + assert parsed.keys_to_actions == {Keys.SPACE.value: {"jump"}} + assert parsed.keys_to_axes == {Keys.LEFT.value: {"move"}} diff --git a/tests/unit/window/test_window.py b/tests/unit/window/test_window.py index 47116db3dd..1c40109f4f 100644 --- a/tests/unit/window/test_window.py +++ b/tests/unit/window/test_window.py @@ -95,3 +95,10 @@ def test_start_finish_render(window): # Only allowed to call finish_render once with pytest.raises(RuntimeError): arcade.finish_render() + + +def test_camera_cannot_be_assigned(window: arcade.Window): + # pyglet draws with window.camera, so it has to stay the default camera + with pytest.raises(AttributeError, match="world_camera"): + window.camera = arcade.Camera2D() + assert window.camera is window.default_camera From e21d79e23597e872e7e3e32369176dd594acd0bc Mon Sep 17 00:00:00 2001 From: Paul V Craven Date: Thu, 8 Oct 2026 09:03:29 -0500 Subject: [PATCH 2/2] Sort the pyglet.enums import Co-Authored-By: Claude Opus 5.5 --- arcade/application.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arcade/application.py b/arcade/application.py index 349801077a..286c0ae268 100644 --- a/arcade/application.py +++ b/arcade/application.py @@ -20,8 +20,8 @@ import pyglet.config.gl import pyglet.window.mouse -from pyglet.enums import GraphicsAPI from pyglet.display.base import Screen, ScreenMode +from pyglet.enums import GraphicsAPI from pyglet.event import EVENT_HANDLE_STATE, EVENT_UNHANDLED from pyglet.window import MouseCursor