diff --git a/arcade/application.py b/arcade/application.py index d32397ff29..41cabdfc68 100644 --- a/arcade/application.py +++ b/arcade/application.py @@ -20,7 +20,6 @@ from arcade.clock import GLOBAL_CLOCK, GLOBAL_FIXED_CLOCK, _setup_clock, _setup_fixed_clock from arcade.color import TRANSPARENT_BLACK from arcade.context import ArcadeContext -from arcade.sections import SectionManager from arcade.types import LBWH, Color, Rect, RGBANormalized, RGBOrA255 from arcade.utils import is_raspberry_pi from arcade.window_commands import get_display_size, set_window @@ -979,37 +978,21 @@ def show_view(self, new_view: View) -> None: # remove previously shown view's handlers if self._current_view is not None: self._current_view.on_hide_view() - if self._current_view.has_sections: - self.remove_handlers(self._current_view.section_manager) self.remove_handlers(self._current_view) # push new view's handlers self._current_view = new_view - if new_view.has_sections: - section_manager_managed_events = new_view.section_manager.managed_events - section_handlers = { - event_type: getattr(new_view.section_manager, event_type, None) - for event_type in section_manager_managed_events - } - if section_handlers: - self.push_handlers(**section_handlers) - else: - section_manager_managed_events = set() # Note: Excluding on_show because this even can trigger multiple times. # It should only be called once when the view is shown. view_handlers = { event_type: getattr(new_view, event_type, None) for event_type in self.event_types - if event_type != "on_show" - and event_type not in section_manager_managed_events - and hasattr(new_view, event_type) + if event_type != "on_show" and hasattr(new_view, event_type) } if view_handlers: self.push_handlers(**view_handlers) self._current_view.on_show_view() - if self._current_view.has_sections: - self._current_view.section_manager.on_show_view() # Note: After the View has been pushed onto pyglet's stack of event handlers # (via push_handlers()), pyglet @@ -1028,18 +1011,9 @@ def hide_view(self) -> None: return self._current_view.on_hide_view() - if self._current_view.has_sections: - self._current_view.section_manager.on_hide_view() - self.remove_handlers(self._current_view.section_manager) self.remove_handlers(self._current_view) self._current_view = None - # def _create(self) -> None: - # super()._create() - - # def _recreate(self, changes) -> None: - # super()._recreate(changes) - def flip(self) -> None: """ Present the rendered content to the screen. @@ -1249,7 +1223,7 @@ class View: Subclassing the window is very inflexible since you can't easily switch your update and draw logic. - A view is a way to encapsulate that logic so you can easily switch between + A view is a way to encapsulate that logic, so you can easily switch between different parts of your game. Maybe you have a title screen, a game screen, and a game over screen. Each of these could be a different view. @@ -1261,46 +1235,6 @@ class View: def __init__(self, window: Window | None = None) -> None: self.window = arcade.get_window() if window is None else window - self.key: int | None = None - self._section_manager: SectionManager | None = None - - @property - def section_manager(self) -> SectionManager: - """The section manager for this view. - - If the view has section manager one will be created. - """ - if self._section_manager is None: - self._section_manager = SectionManager(self) - return self._section_manager - - @property - def has_sections(self) -> bool: - """Returns ``True`` if this view has sections.""" - if self._section_manager is None: - return False - else: - return self.section_manager.has_sections - - def add_section( - self, - section: arcade.Section, - at_index: int | None = None, - at_draw_order: int | None = None, - ) -> None: - """ - Adds a section to the view Section Manager. - - Args: - section: - The section to add to this section manager - at_index (optional): - The index to insert the section for event capture and - update events. If ``None`` it will be added at the end. - at_draw_order (optional): - Inserts the section in a specific draw order. Overwrites section.draw_order - """ - self.section_manager.add_section(section, at_index, at_draw_order) def clear( self, diff --git a/arcade/examples/sections_demo_1.py b/arcade/examples/sections_demo_1.py index 202e92b9c8..ac314aa461 100644 --- a/arcade/examples/sections_demo_1.py +++ b/arcade/examples/sections_demo_1.py @@ -15,13 +15,15 @@ If Python and Arcade are installed, this example can be run from the command line with: python -m arcade.examples.sections_demo_1 """ + from __future__ import annotations import arcade +from arcade import SectionManager class Box(arcade.SpriteSolidColor): - """ This is a Solid Sprite that represents a GREEN Box on the screen """ + """This is a Solid Sprite that represents a GREEN Box on the screen""" def __init__(self, section): super().__init__(100, 100, color=arcade.color.APPLE_GREEN) @@ -47,8 +49,7 @@ class ScreenPart(arcade.Section): boundaries (left, bottom, etc.) """ - def __init__(self, left: int, bottom: int, width: int, height: int, - **kwargs): + def __init__(self, left: int, bottom: int, width: int, height: int, **kwargs): super().__init__(left, bottom, width, height, **kwargs) self.selected: bool = False # if this section is selected @@ -66,19 +67,26 @@ def on_update(self, delta_time: float): self.box.on_update(delta_time) def on_draw(self): - """ Draw this section """ + """Draw this section""" if self.selected: # Section is selected when mouse is within its boundaries - arcade.draw_lrbt_rectangle_filled(self.left, self.right, self.bottom, - self.top, arcade.color.GRAY) - arcade.draw_text(f'You\'re are on the {self.name}', self.left + 30, - self.top - 50, arcade.color.BLACK, 16) + arcade.draw_lrbt_rectangle_filled( + self.left, self.right, self.bottom, self.top, arcade.color.GRAY + ) + arcade.draw_text( + f"You're are on the {self.name}", + self.left + 30, + self.top - 50, + arcade.color.BLACK, + 16, + ) # draw the box arcade.draw_sprite(self.box) - def on_mouse_drag(self, x: float, y: float, dx: float, dy: float, - _buttons: int, _modifiers: int): + def on_mouse_drag( + self, x: float, y: float, dx: float, dy: float, _buttons: int, _modifiers: int + ): # if we hold a box, then whe move it at the same rate the mouse moves if self.hold_box: self.hold_box.position = x, y @@ -110,28 +118,43 @@ def on_mouse_leave(self, x: float, y: float): class GameView(arcade.View): - def __init__(self): super().__init__() # add sections to the view + self.section_manager = SectionManager(self) # 1) First section holds half of the screen - self.add_section(ScreenPart(0, 0, self.window.width / 2, - self.window.height, name='Left')) + self.section_manager.add_section( + ScreenPart(0, 0, self.window.width / 2, self.window.height, name="Left") + ) # 2) Second section holds the other half of the screen - self.add_section(ScreenPart(self.window.width / 2, 0, - self.window.width / 2, self.window.height, - name='Right')) + self.section_manager.add_section( + ScreenPart( + self.window.width / 2, 0, self.window.width / 2, self.window.height, name="Right" + ) + ) + + def on_show_view(self) -> None: + self.section_manager.enable() + + def on_hide_view(self) -> None: + self.section_manager.disable() def on_draw(self): # clear the screen self.clear(color=arcade.color.BEAU_BLUE) # draw a line separating each Section - arcade.draw_line(self.window.width / 2, 0, self.window.width / 2, - self.window.height, arcade.color.BLACK, 1) + arcade.draw_line( + self.window.width / 2, + 0, + self.window.width / 2, + self.window.height, + arcade.color.BLACK, + 1, + ) def main(): @@ -148,5 +171,5 @@ def main(): window.run() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/arcade/examples/sections_demo_2.py b/arcade/examples/sections_demo_2.py index 4a7b4b9e0e..06a9a24198 100644 --- a/arcade/examples/sections_demo_2.py +++ b/arcade/examples/sections_demo_2.py @@ -18,9 +18,10 @@ If Python and Arcade are installed, this example can be run from the command line with: python -m arcade.examples.sections_demo_2 """ + import random -import arcade +import arcade from arcade.color import BLACK, BLUE, RED, BEAU_BLUE, GRAY from arcade.key import W, S, UP, DOWN @@ -35,9 +36,8 @@ class Player(arcade.Section): """ def __init__( - self, left: int, bottom: int, width: int, height: int, - key_up: int, key_down: int, **kwargs - ): + self, left: int, bottom: int, width: int, height: int, key_up: int, key_down: int, **kwargs + ): super().__init__( left, bottom, @@ -67,19 +67,24 @@ def on_update(self, delta_time: float): def on_draw(self): # draw sections info and score - if self.name == 'Left': - keys = 'W and S' + if self.name == "Left": + keys = "W and S" start_x = self.left + 5 else: - keys = 'UP and DOWN' + keys = "UP and DOWN" start_x = self.left - 290 arcade.draw_text( - f'Player {self.name} (move paddle with: {keys})', - start_x, self.top - 20, BLUE, 9, + f"Player {self.name} (move paddle with: {keys})", + start_x, + self.top - 20, + BLUE, + 9, ) arcade.draw_text( - f'Score: {self.score}', self.left + 20, - self.bottom + 20, BLUE, + f"Score: {self.score}", + self.left + 20, + self.bottom + 20, + BLUE, ) # draw the paddle @@ -98,7 +103,6 @@ def on_key_release(self, _symbol: int, _modifiers: int): class Pong(arcade.View): - def __init__(self): super().__init__() @@ -108,15 +112,22 @@ def __init__(self): # we store each Section self.left_player: Player = Player( - 0, 0, PLAYER_SECTION_WIDTH, self.window.height, key_up=W, - key_down=S, name='Left') + 0, 0, PLAYER_SECTION_WIDTH, self.window.height, key_up=W, key_down=S, name="Left" + ) self.right_player: Player = Player( - self.window.width - PLAYER_SECTION_WIDTH, 0, PLAYER_SECTION_WIDTH, - self.window.height, key_up=UP, key_down=DOWN, name='Right') + self.window.width - PLAYER_SECTION_WIDTH, + 0, + PLAYER_SECTION_WIDTH, + self.window.height, + key_up=UP, + key_down=DOWN, + name="Right", + ) # add the sections to the SectionManager so Sections start to work - self.add_section(self.left_player) - self.add_section(self.right_player) + self.section_manager = arcade.SectionManager(self) + self.section_manager.add_section(self.left_player) + self.section_manager.add_section(self.right_player) # add each paddle to the sprite list self.paddles.append(self.left_player.paddle) @@ -139,6 +150,12 @@ def setup(self): self.left_player.setup() self.right_player.setup() + def on_show_view(self) -> None: + self.section_manager.enable() + + def on_hide_view(self) -> None: + self.section_manager.disable() + def on_update(self, delta_time: float): self.ball.update() # update the ball @@ -168,7 +185,7 @@ def on_update(self, delta_time: float): self.end_game(self.left_player) def end_game(self, winner: Player): - """ Called when one player wins """ + """Called when one player wins""" winner.score += 1 # increment the winner score self.setup() # prepare a new game @@ -185,7 +202,7 @@ def on_draw(self): def main(): # create the window - window = arcade.Window(title='Two player simple Pong with Sections!') + window = arcade.Window(title="Two player simple Pong with Sections!") # create the custom View game = Pong() @@ -200,5 +217,5 @@ def main(): window.run() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/arcade/examples/sections_demo_3.py b/arcade/examples/sections_demo_3.py index e25c476559..7ff1e458e0 100644 --- a/arcade/examples/sections_demo_3.py +++ b/arcade/examples/sections_demo_3.py @@ -21,26 +21,28 @@ If Python and Arcade are installed, this example can be run from the command line with: python -m arcade.examples.sections_demo_3 """ + from __future__ import annotations + from math import sqrt import arcade -from arcade import Section +from arcade import Section, SectionManager from arcade.types import Color INFO_BAR_HEIGHT = 40 PANEL_WIDTH = 200 SPRITE_SPEED = 1 -COLOR_LIGHT = Color.from_hex_string('#D9BBA0') -COLOR_DARK = Color.from_hex_string('#0D0D0D') -COLOR_1 = Color.from_hex_string('#2A1459') -COLOR_2 = Color.from_hex_string('#4B89BF') -COLOR_3 = Color.from_hex_string('#03A688') +COLOR_LIGHT = Color.from_hex_string("#D9BBA0") +COLOR_DARK = Color.from_hex_string("#0D0D0D") +COLOR_1 = Color.from_hex_string("#2A1459") +COLOR_2 = Color.from_hex_string("#4B89BF") +COLOR_3 = Color.from_hex_string("#03A688") class Ball(arcade.SpriteCircle): - """ The moving ball """ + """The moving ball""" def __init__(self, radius, color): super().__init__(radius, color) @@ -54,7 +56,7 @@ def speed(self): class ModalSection(Section): - """ A modal section that represents a popup that waits for user input """ + """A modal section that represents a popup that waits for user input""" def __init__(self, left: int, bottom: int, width: int, height: int): super().__init__(left, bottom, width, height, modal=True, enabled=False) @@ -66,34 +68,39 @@ def __init__(self, left: int, bottom: int, width: int, height: int): def on_draw(self): # draw modal frame and button - arcade.draw_lrbt_rectangle_filled(self.left, self.right, self.bottom, - self.top, arcade.color.GRAY) - arcade.draw_lrbt_rectangle_outline(self.left, self.right, self.bottom, - self.top, arcade.color.WHITE) + arcade.draw_lrbt_rectangle_filled( + self.left, self.right, self.bottom, self.top, arcade.color.GRAY + ) + arcade.draw_lrbt_rectangle_outline( + self.left, self.right, self.bottom, self.top, arcade.color.WHITE + ) self.draw_button() def draw_button(self): # draws the button and button text arcade.draw_sprite(self.button) - arcade.draw_text('Close Modal', self.button.left + 5, - self.button.bottom + self.button.height / 2, - arcade.color.WHITE) + arcade.draw_text( + "Close Modal", + self.button.left + 5, + self.button.bottom + self.button.height / 2, + arcade.color.WHITE, + ) def on_resize(self, width: int, height: int): - """ set position on screen resize """ + """set position on screen resize""" self.left = width // 3 self.bottom = (height // 2) - self.height // 2 pos = self.left + self.width / 2, self.bottom + self.height / 2 self.button.position = pos def on_mouse_press(self, x: float, y: float, button: int, modifiers: int): - """ Check if the button is pressed """ + """Check if the button is pressed""" if self.button.collides_with_point((x, y)): self.enabled = False class InfoBar(Section): - """ This is the top bar of the screen where info is showed """ + """This is the top bar of the screen where info is showed""" @property def ball(self): @@ -101,21 +108,30 @@ def ball(self): def on_draw(self): # draw game info - arcade.draw_lrbt_rectangle_filled(self.left, self.right, self.bottom, - self.top, COLOR_DARK) - arcade.draw_lrbt_rectangle_outline(self.left, self.right, self.bottom, - self.top, COLOR_LIGHT) - arcade.draw_text(f'Ball bounce count: {self.ball.bounce_count}', - self.left + 20, self.top - self.height / 1.6, - COLOR_LIGHT) + arcade.draw_lrbt_rectangle_filled(self.left, self.right, self.bottom, self.top, COLOR_DARK) + arcade.draw_lrbt_rectangle_outline( + self.left, self.right, self.bottom, self.top, COLOR_LIGHT + ) + arcade.draw_text( + f"Ball bounce count: {self.ball.bounce_count}", + self.left + 20, + self.top - self.height / 1.6, + COLOR_LIGHT, + ) ball_change_axis = self.ball.change_x, self.ball.change_y - arcade.draw_text(f'Ball change in axis: {ball_change_axis}', - self.left + 220, self.top - self.height / 1.6, - COLOR_LIGHT) - arcade.draw_text(f'Ball speed: {self.ball.speed} pixels/second', - self.left + 480, self.top - self.height / 1.6, - COLOR_LIGHT) + arcade.draw_text( + f"Ball change in axis: {ball_change_axis}", + self.left + 220, + self.top - self.height / 1.6, + COLOR_LIGHT, + ) + arcade.draw_text( + f"Ball speed: {self.ball.speed} pixels/second", + self.left + 480, + self.top - self.height / 1.6, + COLOR_LIGHT, + ) def on_resize(self, width: int, height: int): # stick to the top @@ -124,10 +140,9 @@ def on_resize(self, width: int, height: int): class Panel(Section): - """This is the Panel to the right where buttons and info is showed """ + """This is the Panel to the right where buttons and info is showed""" - def __init__(self, left: int, bottom: int, width: int, height: int, - **kwargs): + def __init__(self, left: int, bottom: int, width: int, height: int, **kwargs): super().__init__(left, bottom, width, height, **kwargs) # create buttons @@ -144,31 +159,39 @@ def new_button(color): return arcade.SpriteSolidColor(100, 50, color=color) def draw_button_stop(self): - arcade.draw_text('Press button to stop the ball', self.left + 10, - self.top - 40, COLOR_LIGHT, 10) + arcade.draw_text( + "Press button to stop the ball", self.left + 10, self.top - 40, COLOR_LIGHT, 10 + ) arcade.draw_sprite(self.button_stop) def draw_button_toggle_info_bar(self): - arcade.draw_text('Press to toggle info_bar', self.left + 10, - self.top - 140, COLOR_LIGHT, 10) + arcade.draw_text( + "Press to toggle info_bar", self.left + 10, self.top - 140, COLOR_LIGHT, 10 + ) arcade.draw_sprite(self.button_toggle_info_bar) def draw_button_show_modal(self): arcade.draw_sprite(self.button_show_modal) - arcade.draw_text('Show Modal', self.left - 37 + self.width / 2, - self.bottom + 95, COLOR_DARK, 10) + arcade.draw_text( + "Show Modal", self.left - 37 + self.width / 2, self.bottom + 95, COLOR_DARK, 10 + ) def on_draw(self): - arcade.draw_lrbt_rectangle_filled(self.left, self.right, self.bottom, - self.top, COLOR_DARK) - arcade.draw_lrbt_rectangle_outline(self.left, self.right, self.bottom, - self.top, COLOR_LIGHT) + arcade.draw_lrbt_rectangle_filled(self.left, self.right, self.bottom, self.top, COLOR_DARK) + arcade.draw_lrbt_rectangle_outline( + self.left, self.right, self.bottom, self.top, COLOR_LIGHT + ) self.draw_button_stop() self.draw_button_toggle_info_bar() if self.pressed_key: - arcade.draw_text(f'Pressed key code: {self.pressed_key}', - self.left + 10, self.top - 240, COLOR_LIGHT, 9) + arcade.draw_text( + f"Pressed key code: {self.pressed_key}", + self.left + 10, + self.top - 240, + COLOR_LIGHT, + 9, + ) self.draw_button_show_modal() @@ -200,10 +223,9 @@ def on_key_release(self, _symbol: int, _modifiers: int): class Map(Section): - """ This represents the place where the game takes place """ + """This represents the place where the game takes place""" - def __init__(self, left: int, bottom: int, width: int, height: int, - **kwargs): + def __init__(self, left: int, bottom: int, width: int, height: int, **kwargs): super().__init__(left, bottom, width, height, **kwargs) self.ball = Ball(20, COLOR_3) @@ -214,7 +236,6 @@ def __init__(self, left: int, bottom: int, width: int, height: int, self.pressed_key: int | None = None def on_update(self, delta_time: float): - if self.pressed_key: if self.pressed_key == arcade.key.UP: self.ball.change_y += SPRITE_SPEED @@ -235,10 +256,10 @@ def on_update(self, delta_time: float): self.ball.bounce_count += 1 def on_draw(self): - arcade.draw_lrbt_rectangle_filled(self.left, self.right, self.bottom, - self.top, COLOR_DARK) - arcade.draw_lrbt_rectangle_outline(self.left, self.right, self.bottom, - self.top, COLOR_LIGHT) + arcade.draw_lrbt_rectangle_filled(self.left, self.right, self.bottom, self.top, COLOR_DARK) + arcade.draw_lrbt_rectangle_outline( + self.left, self.right, self.bottom, self.top, COLOR_LIGHT + ) self.sprite_list.draw() def on_key_press(self, symbol: int, modifiers: int): @@ -253,7 +274,7 @@ def on_resize(self, width: int, height: int): class GameView(arcade.View): - """ The game itself """ + """The game itself""" def __init__(self): super().__init__() @@ -263,7 +284,8 @@ def __init__(self): self.modal_section = ModalSection( (self.window.width / 2) - 150, (self.window.height / 2) - 100, - 300, 200, + 300, + 200, ) # we set accept_keyboard_events to False (default to True) @@ -277,18 +299,28 @@ def __init__(self): # as prevent_dispatch is on by default, we let pass the events to the # following Section: the map - self.panel = Panel(self.window.width - PANEL_WIDTH, 0, PANEL_WIDTH, - self.window.height - INFO_BAR_HEIGHT, - prevent_dispatch={False}) - self.map = Map(0, 0, self.window.width - PANEL_WIDTH, - self.window.height - INFO_BAR_HEIGHT) + self.panel = Panel( + self.window.width - PANEL_WIDTH, + 0, + PANEL_WIDTH, + self.window.height - INFO_BAR_HEIGHT, + prevent_dispatch={False}, + ) + self.map = Map(0, 0, self.window.width - PANEL_WIDTH, self.window.height - INFO_BAR_HEIGHT) # add the sections + self.section_manager = SectionManager(self) self.section_manager.add_section(self.modal_section) self.section_manager.add_section(self.info_bar) self.section_manager.add_section(self.panel) self.section_manager.add_section(self.map) + def on_show_view(self) -> None: + self.section_manager.enable() + + def on_hide_view(self) -> None: + self.section_manager.disable() + def on_draw(self): self.clear() @@ -302,5 +334,5 @@ def main(): window.run() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/arcade/future/__init__.py b/arcade/future/__init__.py index 123df618ce..1f42c56507 100644 --- a/arcade/future/__init__.py +++ b/arcade/future/__init__.py @@ -2,7 +2,8 @@ from . import light from . import input from . import background +from . import splash from .texture_render_target import RenderTargetTexture -__all__ = ["video", "light", "input", "background", "RenderTargetTexture"] +__all__ = ["video", "light", "input", "background", "RenderTargetTexture", "splash"] diff --git a/arcade/sections.py b/arcade/sections.py index cf8ff7f5e1..f4a7318571 100644 --- a/arcade/sections.py +++ b/arcade/sections.py @@ -21,17 +21,19 @@ class Section: A Section represents a rectangular portion of the viewport Events are dispatched to the section based on it's position on the screen. + A section can only be added to a single SectionManager. + Args: left: the left position of this section bottom: the bottom position of this section width: the width of this section height: the height of this section name: the name of this section - bool | Iterable accept_keyboard_keys: whether or not this section + bool | Iterable accept_keyboard_keys: whether this section captures keyboard keys through. keyboard events. If the param is an iterable means the keyboard keys that are captured in press/release events: for example: ``[arcade.key.UP, arcade.key.DOWN]`` will only capture this two keys - bool Iterable accept_mouse_events: whether or not this section + bool Iterable accept_mouse_events: whether this section captures mouse events. If the param is an iterable means the mouse events that are captured. for example: ``['on_mouse_press', 'on_mouse_release']`` will only capture this two events. @@ -75,6 +77,7 @@ def __init__( # parent view: set by the SectionManager. Protected, you should not change # section.view manually self._view: View | None = None + self._section_manager: SectionManager | None = None # section options self._enabled: bool = enabled # enables or disables this section @@ -134,8 +137,8 @@ def view(self): @property def section_manager(self) -> SectionManager | None: - """Returns the section manager""" - return self._view.section_manager if self._view else None + """Returns the section manager this section is added to""" + return self._section_manager @property def enabled(self) -> bool: @@ -168,16 +171,6 @@ def draw_order(self) -> int: """ return self._draw_order - @draw_order.setter - def draw_order(self, value: int) -> None: - """ - Sets this section draw order - The lower the number the earlier this section will get draw - """ - self._draw_order = value - if self.section_manager is not None: - self.section_manager.sort_sections_draw_order() - @property def left(self) -> int: """Left edge of this section""" @@ -514,16 +507,30 @@ def disable(self) -> None: Disable all sections Disabling a section will trigger section.on_hide_section """ + self.view.window.remove_handlers(self) + for section in self._sections: section.enabled = False def enable(self) -> None: """ - Enables all sections + Registers event handlers to the window and enables all sections. + Enabling a section will trigger section.on_show_section """ - for section in self._sections: - section.enabled = True + section_handlers = { + event_type: getattr(self, event_type, None) for event_type in self.managed_events + } + if section_handlers: + self.view.window.push_handlers(**section_handlers) + + for section in self.sections: + # sections are enabled by default + if section.enabled: + section.on_show_section() + else: + section.enabled = True + # enabling a section will trigger on_show_section def get_section_by_name(self, name: str) -> Section | None: """ @@ -558,7 +565,13 @@ def add_section( """ if not isinstance(section, Section): raise ValueError("You can only add Section instances") - section._view = self.view # modify view param from section + if section.section_manager is not None: + raise ValueError("Section is already added to a SectionManager") + + # set the view and section manager + section._view = self.view + section._section_manager = self + if at_index is None: self._sections.append(section) else: @@ -568,7 +581,8 @@ def add_section( if at_draw_order is None: self.sort_sections_draw_order() else: - section.draw_order = at_draw_order # this will trigger self.sort_section_draw_order + section._draw_order = at_draw_order # this will trigger self.sort_section_draw_order + self.sort_sections_draw_order() # trigger on_show_section if the view is the current one and section is enabled: if self.is_current_view and section.enabled: @@ -617,7 +631,7 @@ def clear_sections(self) -> None: self._sections = [] self._sections_draw = [] - def on_update(self, delta_time: float) -> None: + def on_update(self, delta_time: float) -> bool: """ Called on each event loop. @@ -635,14 +649,19 @@ def on_update(self, delta_time: float) -> None: if self.view_update_first is False: self.view.on_update(delta_time) - def on_draw(self) -> None: + # prevent further event dispatch to the view, which was already invoked + return True + + def on_draw(self) -> bool: """ Called on each event loop to draw. It automatically calls camera.use() for each section that has a camera and resets the camera effects by calling the default SectionManager camera - afterwards if needed. The SectionManager camera defaults to a camera that + afterward if needed. The SectionManager camera defaults to a camera that has the viewport and projection for the whole screen. + + This method will consume the on_draw event, to prevent further on_draw calls on the view. """ if self.view_draw_first is True: self.view.on_draw() @@ -659,7 +678,10 @@ def on_draw(self) -> None: if self.view_draw_first is False: self.view.on_draw() - def on_resize(self, width: int, height: int) -> None: + # prevent further event dispatch to the view, which was already invoked + return True + + def on_resize(self, width: int, height: int) -> bool: """ Called when the window is resized. @@ -676,6 +698,9 @@ def on_resize(self, width: int, height: int) -> None: if self.view_resize_first is False: self.view.on_resize(width, height) # call resize on the view + # prevent further event dispatch to the view, which was already invoked + return True + def disable_all_keyboard_events(self) -> None: """Removes the keyboard event handling from all sections""" for section in self.sections: @@ -799,6 +824,7 @@ def dispatch_mouse_event( if method: # call the view method prevent_dispatch_view = method(x, y, *args, **kwargs) + return prevent_dispatch_view or prevent_dispatch def dispatch_keyboard_event(self, event: str, *args, **kwargs) -> bool | None: @@ -1048,21 +1074,3 @@ def on_key_release(self, *args, **kwargs) -> bool | None: ``EVENT_HANDLED`` or ``EVENT_UNHANDLED``, or whatever the dispatched method returns """ return self.dispatch_keyboard_event("on_key_release", *args, **kwargs) - - def on_show_view(self) -> None: - """ - Called when the view is shown. The :py:meth:`View.on_show_view` is called before - this by the :py:meth:`Window.show_view` method. - """ - for section in self.sections: - if section.enabled: - section.on_show_section() - - def on_hide_view(self) -> None: - """ - Called when the view is hide - The View.on_hide_view is called before this by the Window.hide_view method - """ - for section in self.sections: - if section.enabled: - section.on_hide_section() diff --git a/doc/programming_guide/sections.rst b/doc/programming_guide/sections.rst index c88b4a78ae..9b1ae7fdfd 100644 --- a/doc/programming_guide/sections.rst +++ b/doc/programming_guide/sections.rst @@ -81,6 +81,8 @@ This is what looks like using Sections: .. code:: py + import arcade + class Map(arcade.Section): # ... @@ -105,8 +107,15 @@ This is what looks like using Sections: self.map_section = Map(0, 0, 700, self.window.height) self.side_section = Side(700, 0, 100, self.window.height) - self.add_section(self.map_section) - self.add_section(self.side_section) + self.sm = arcade.SectionManager() + self.sm.add_section(self.map_section) + self.sm.add_section(self.side_section) + + def on_show_view(self) -> None: + self.sm.enable() + + def on_hide_view(self) -> None: + self.sm.disable() # ... @@ -116,8 +125,12 @@ How to work with Sections To work with sections you first need to have a :class:`~arcade.View`. Sections depend on Views and are handled by a special :class:`~arcade.SectionManager` inside the -:class:`~arcade.View`. Don't worry, 99% of the time you won't need to interact with the -:class:`~arcade.SectionManager`. +:class:`~arcade.View`. The :class:`~arcade.SectionManager` will handle all the events and +dispatch them to the sections. The :class:`~arcade.SectionManager` will also handle the +drawing order of the sections. + +You will have to enable and disable the :class:`~arcade.SectionManager` in the :class:`~arcade.View` +``on_show_view`` and ``on_hide_view`` methods. To create a :class:`~arcade.Section` start by inheriting from :py:class:`~arcade.Section`. @@ -285,10 +298,17 @@ Lets look what this configuration may look: self.popup = PopUp(message='', popup_left, popup_bottom, popup_width, popup_height, enabled=False, modal=True) - self.add_section(self.map) - self.add_section(self.menu) - self.add_section(self.panel) - self.add_section(self.popup) + self.sm = arcade.SectionManager() + self.sm.add_section(self.map) + self.sm.add_section(self.menu) + self.sm.add_section(self.panel) + self.sm.add_section(self.popup) + + def on_show_view(self) -> None: + self.sm.section_manager.enable() + + def on_hide_view(self) -> None: + self.sm.section_manager.disable() def close(): self.popup.message = 'Are you sure you want to close the view?' @@ -344,9 +364,6 @@ Behind the scenes, when sections are added to the :class:`~arcade.View` the :class:`~arcade.SectionManager` is what will handle all events instead of the :class:`~arcade.View` itself. -You can access the :class:`~arcade.SectionManager` by accessing the ``View.section_manager``. -Note that if you don't use Sections, the section manager inside the View will not be used nor created. - Usually you won't need to work with the :class:`~arcade.SectionManager`, but there are some cases where you will need to work with it. diff --git a/tests/conftest.py b/tests/conftest.py index 6a0b290d26..8f5358abfa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -29,7 +29,9 @@ def create_window(width=1280, height=720, caption="Testing", **kwargs): global WINDOW if not WINDOW: - WINDOW = REAL_WINDOW_CLASS(width=width, height=height, title=caption, vsync=False, antialiasing=False) + WINDOW = REAL_WINDOW_CLASS( + width=width, height=height, title=caption, vsync=False, antialiasing=False + ) WINDOW.set_vsync(False) # This value is being monkey-patched into the Window class so that tests can identify if we are using # arcade-accelerate easily in case they need to disable something when it is enabled. @@ -164,7 +166,7 @@ def projection(self, projection): @property def viewport(self): - return self.window.viewport + return self.window.viewport @viewport.setter def viewport(self, viewport): @@ -282,11 +284,11 @@ def default_camera(self): def use(self): self.window.use() - def push_handlers(self, *handlers): - self.window.push_handlers(*handlers) + def push_handlers(self, *args, **kwargs): + self.window.push_handlers(*args, **kwargs) - def remove_handlers(self, *handlers): - self.window.remove_handlers(*handlers) + def remove_handlers(self, *args, **kwargs): + self.window.remove_handlers(*args, **kwargs) def run(self): self.window.run() @@ -309,7 +311,6 @@ def fixed_delta_time(self) -> float: return self.window._fixed_rate - @pytest.fixture(scope="function") def window_proxy(): """Monkey patch the open_window function and return a WindowTools instance.""" @@ -364,7 +365,9 @@ def read_pixel(self, x, y, components=3) -> tuple[int, int, int, int] | tuple[in def read_region(self, rect: Rect) -> list[tuple[int, int, int, int]]: """Read a region of RGBA pixels from the offscreen buffer""" - data = self.fbo.read(components=4, viewport=(rect.left, rect.bottom, rect.width, rect.height)) + data = self.fbo.read( + components=4, viewport=(rect.left, rect.bottom, rect.width, rect.height) + ) return [ ( int.from_bytes(data[i : i + 4], "little"), diff --git a/tests/integration/examples/test_line_lengths.py b/tests/integration/examples/test_line_lengths.py index 342b27d7de..50b65d42a2 100644 --- a/tests/integration/examples/test_line_lengths.py +++ b/tests/integration/examples/test_line_lengths.py @@ -29,7 +29,7 @@ def is_ignored(path: Path): def test_line_lengths(): paths = EXAMPLE_ROOT.glob("**/*.py") - regex = re.compile("^.{99}.*$") + regex = re.compile("^.{100}.*$") grand_total = 0 file_count = 0 diff --git a/tests/unit/section/__init__.py b/tests/unit/section/__init__.py new file mode 100644 index 0000000000..6ad18c61e9 --- /dev/null +++ b/tests/unit/section/__init__.py @@ -0,0 +1,67 @@ +import arcade +from arcade import Section, SectionManager + + +class RecorderView(arcade.View): + def __init__(self): + super().__init__() + + self.section_manager = SectionManager(self) + self.events = [] + + def on_mouse_enter(self, x: float, y: float): + self.events.append("on_mouse_enter") + + def on_mouse_motion(self, x: float, y: float, dx: float, dy: float): + self.events.append("on_mouse_motion") + + def on_mouse_leave(self, x: float, y: float): + self.events.append("on_mouse_leave") + + def on_mouse_press(self, x: float, y: float, button: int, modifiers: int): + self.events.append("on_mouse_press") + + def on_mouse_release(self, x: float, y: float, button: int, modifiers: int): + self.events.append("on_mouse_release") + + def on_show_view(self) -> None: + self.section_manager.enable() + + def on_hide_view(self) -> None: + self.section_manager.disable() + + def on_update(self, delta_time: float): + self.events.append("on_update") + + def on_draw(self): + self.events.append("on_draw") + + +class RecorderSection(Section): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.events = [] + + def on_mouse_enter(self, x: float, y: float): + self.events.append("on_mouse_enter") + + def on_mouse_leave(self, x: float, y: float): + self.events.append("on_mouse_leave") + + def on_mouse_press(self, x: float, y: float, button: int, modifiers: int): + self.events.append("on_mouse_press") + + def on_mouse_release(self, x: float, y: float, button: int, modifiers: int): + self.events.append("on_mouse_release") + + def on_update(self, delta_time: float): + self.events.append("on_update") + + def on_draw(self): + self.events.append("on_draw") + + def on_show_section(self): + self.events.append("on_show_section") + + def on_hide_section(self): + self.events.append("on_hide_section") diff --git a/tests/unit/section/test_event_handing.py b/tests/unit/section/test_event_handing.py new file mode 100644 index 0000000000..56d1a8192a --- /dev/null +++ b/tests/unit/section/test_event_handing.py @@ -0,0 +1,93 @@ +import arcade +from arcade import View +from tests.unit.section import RecorderSection, RecorderView + + +def test_section_manager_enable_event_handling(window): + # GIVEN + view = RecorderView() + manager = view.section_manager + + # SETUP + recorder_section = RecorderSection( + *window.rect.lbwh, + ) + manager.add_section(section=recorder_section) + window.show_view(view) + + # WHEN + + window.dispatch_event("on_mouse_motion", 10, 10, 0, 0) + # mouse motion will trigger mouse enter automatically + window.dispatch_event("on_mouse_press", 11, 11, arcade.MOUSE_BUTTON_LEFT, 0) + window.dispatch_event("on_mouse_release", 11, 11, arcade.MOUSE_BUTTON_LEFT, 0) + window.dispatch_event("on_draw") + window.dispatch_event("on_update", 1 / 60) + window.dispatch_event("on_mouse_leave", 10, 10) + window.dispatch_events() + + # THEN + assert recorder_section.events == [ + "on_show_section", + "on_mouse_enter", + "on_mouse_press", + "on_mouse_release", + "on_draw", + "on_update", + "on_mouse_leave", + ] + + +def test_sections_receive_callback_when_manager_enabled_and_disabled(window): + # GIVEN + view = RecorderView() + manager = view.section_manager + + # SETUP + recorder_section = RecorderSection( + *window.rect.lbwh, + ) + manager.add_section(section=recorder_section) + window.show_view(view) # will enable the manager + + # THEN + assert recorder_section.events == ["on_show_section"] + + # WHEN + window.show_view(View()) # will disable the manager + + # THEN + assert recorder_section.events == ["on_show_section", "on_hide_section"] + + +def test_view_receives_events_once(window): + # GIVEN + view = RecorderView() + manager = view.section_manager + + # SETUP + recorder_section = RecorderSection( + *window.rect.lbwh, prevent_dispatch_view={False}, prevent_dispatch={True} + ) + manager.add_section(section=recorder_section) + window.show_view(view) + + # WHEN + window.dispatch_event("on_mouse_motion", 10, 10, 0, 0) + window.dispatch_event("on_mouse_press", 10, 10, arcade.MOUSE_BUTTON_LEFT, 0) + window.dispatch_event("on_mouse_release", 10, 10, arcade.MOUSE_BUTTON_LEFT, 0) + window.dispatch_event("on_mouse_leave", 10, 10) + window.dispatch_event("on_draw") + window.dispatch_event("on_update", 1 / 60) + window.dispatch_events() + + # THEN + assert view.events == [ + "on_mouse_enter", + "on_mouse_motion", + "on_mouse_press", + "on_mouse_release", + "on_mouse_leave", + "on_draw", + "on_update", + ]