diff --git a/CHANGELOG.md b/CHANGELOG.md index 2065272db2..67b5559189 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,7 +67,14 @@ API in a way that is not compatible with how it was used in 2.6. * Removed `arcade.gui.widgets.UIPadding` this is now general available in `arcade.gui.widgets.UIWidget` * Removed `arcade.gui.widgets.UITexturePane` this is now general available in `arcade.gui.widgets.UIWidget` * Removed `arcade.gui.widgets.UIAnchorWidget` replaced by `arcade.gui.widgets.UIAnchorLayout` - +* Resources + * removed unsused resources from `resources/gui_basic_assets` + * `items/shield_gold.png` + * `items/sword_gold.png` + * `slider_thumb.png` + * `slider_track.png` + * `toggle/switch_green.png` + * `toggle/switch_red.png` ### Featured Updates * The texture atlas has been heavily reworked to be more efficient. @@ -131,6 +138,10 @@ API in a way that is not compatible with how it was used in 2.6. * Update and add example code. * Iterable (providing direct children) +* Updated widgets + * `arcade.gui.widgets.text.UIInputText` emits `on_change` event when new text input or set + * `arcade.gui.widgets.slider.UITextureSlider` texture names changed to fit general naming pattern + * New widgets: * `arcade.gui.widgets.dropdown.UIDropdown` @@ -152,6 +163,8 @@ API in a way that is not compatible with how it was used in 2.6. * `arcade.gui.UIAnchorLayout` * `arcade.gui.UIGridLayout` [PR1478](https://github.com/pythonarcade/arcade/pull/1478) +* Added color consistent assets to `arcade.resources.gui_basic_assets` +* Provide GUI friendly color constants in `arcade.uicolor` * Replace deprecated usage of `arcade.draw_text` ### Misc Changes diff --git a/arcade/VERSION b/arcade/VERSION index c4ca5c48b2..07884b8456 100644 --- a/arcade/VERSION +++ b/arcade/VERSION @@ -1 +1 @@ -3.0.0-dev.33 \ No newline at end of file +3.0.0-dev.36 \ No newline at end of file diff --git a/arcade/__init__.py b/arcade/__init__.py index be38235b89..05c4afd90e 100644 --- a/arcade/__init__.py +++ b/arcade/__init__.py @@ -228,6 +228,7 @@ def configure_logging(level: int | None = None): # Module imports from arcade import color as color from arcade import csscolor as csscolor +from arcade import uicolor as uicolor from arcade import camera as camera from arcade import key as key from arcade import resources as resources @@ -387,6 +388,7 @@ def configure_logging(level: int | None = None): "rect", "color", "csscolor", + "uicolor", "key", "resources", "types", 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/camera/default.py b/arcade/camera/default.py index 37a673f095..b37e924135 100644 --- a/arcade/camera/default.py +++ b/arcade/camera/default.py @@ -6,7 +6,7 @@ from pyglet.math import Mat4, Vec2, Vec3 from typing_extensions import Self -from arcade.types import Point +from arcade.types import LBWH, Point, Rect from arcade.window_commands import get_window if TYPE_CHECKING: @@ -29,28 +29,28 @@ class ViewportProjector: def __init__( self, - viewport: tuple[int, int, int, int] | None = None, + viewport: Rect | None = None, *, context: ArcadeContext | None = None, ): - self._ctx = context or get_window().ctx - self._viewport = viewport or self._ctx.viewport + self._ctx: ArcadeContext = context or get_window().ctx + self._viewport: Rect = viewport or LBWH(*self._ctx.viewport) self._projection_matrix: Mat4 = Mat4.orthogonal_projection( - 0.0, self._viewport[2], 0.0, self._viewport[3], -100, 100 + 0.0, self._viewport.width, 0.0, self._viewport.height, -100, 100 ) @property - def viewport(self) -> tuple[int, int, int, int]: + def viewport(self) -> Rect: """ The viewport use to derive projection and view matrix. """ return self._viewport @viewport.setter - def viewport(self, viewport: tuple[int, int, int, int]) -> None: + def viewport(self, viewport: Rect) -> None: self._viewport = viewport self._projection_matrix = Mat4.orthogonal_projection( - 0, viewport[2], 0, viewport[3], -100, 100 + 0, viewport.width, 0, viewport.height, -100, 100 ) def use(self) -> None: @@ -60,7 +60,7 @@ def use(self) -> None: """ self._ctx.current_camera = self - self._ctx.viewport = self._viewport + self._ctx.viewport = self.viewport.viewport # get the integer 4-tuple LBWH self._ctx.view_matrix = Mat4() self._ctx.projection_matrix = self._projection_matrix @@ -121,18 +121,19 @@ def use(self) -> None: cache's the window viewport to determine the projection matrix. """ + viewport = self.viewport.viewport # If the viewport is correct and the default camera is in use, # then don't waste time resetting the view and projection matrices - if self._ctx.viewport == self.viewport and self._ctx.current_camera == self: + if self._ctx.viewport == viewport and self._ctx.current_camera == self: return # If the viewport has changed while the default camera is active then the # default needs to update itself. # If it was another camera's viewport being used the default camera should not update. - if self._ctx.viewport != self.viewport and self._ctx.current_camera == self: - self.viewport = self._ctx.viewport + if self._ctx.viewport != viewport and self._ctx.current_camera == self: + self.viewport = LBWH(*self._ctx.viewport) else: - self._ctx.viewport = self.viewport + self._ctx.viewport = viewport self._ctx.current_camera = self diff --git a/arcade/clock.py b/arcade/clock.py index f6b92d6303..6fe82ff69d 100644 --- a/arcade/clock.py +++ b/arcade/clock.py @@ -19,11 +19,11 @@ class Clock: only certain elements rather than everything. Args: - initial_elapsed (float, optional): The amount of time the clock should assume has + initial_elapsed: The amount of time the clock should assume has already occurred. Defaults to 0.0 - initial_tick (int, optional): The number of ticks the clock should assume has already + initial_tick: The number of ticks the clock should assume has already occurred. Defaults to 0. - tick_speed (float, optional): A multiplier on how the 'speed' of time. + tick_speed: A multiplier on how the 'speed' of time. i.e. a value of 0.5 means time elapsed half as fast for this clock. Defaults to 1.0. """ @@ -40,7 +40,7 @@ def tick(self, delta_time: float): Update the clock with the time that has passed since the last tick. Args: - delta_time (float): The amount of time that has passed since the last tick. + delta_time: The amount of time that has passed since the last tick. """ self._tick_delta_time = delta_time * self._tick_speed self._elapsed_time += self._tick_delta_time @@ -51,7 +51,7 @@ def set_tick_speed(self, new_tick_speed: float): Set the speed of time for this clock. Args: - new_tick_speed (float): A multiplier on the 'speed' of time. + new_tick_speed: A multiplier on the 'speed' of time. i.e. a value of 0.5 means time elapsed half as fast for this clock. """ self._tick_speed = new_tick_speed @@ -61,7 +61,7 @@ def time_since(self, time: float) -> float: Calculate the amount of time that has passed since the given time. Args: - time (float): The time to compare against. + time: The time to compare against. """ return self._elapsed_time - time @@ -70,7 +70,7 @@ def ticks_since(self, tick: int) -> int: Calculate the number of ticks that have occurred since the given tick. Args: - tick (int): The tick to compare against. + tick: The tick to compare against. """ return self._tick - tick @@ -123,8 +123,8 @@ class FixedClock(Clock): Arcade provides a global fixed clock which is automatically ticked every update Args: - sibling (Clock): The unfixed clock which this clock will sync with. - fixed_tick_rate (float, optional): The fixed number of seconds that pass + sibling: The unfixed clock which this clock will sync with. + fixed_tick_rate: The fixed number of seconds that pass for this clock every tick. Defaults to ``1.0 / 60.0``. """ @@ -138,7 +138,7 @@ def set_tick_speed(self, new_tick_speed: float): Set the speed of time for this clock. Args: - new_tick_speed (float): A multiplier on the 'speed' of time. + new_tick_speed: A multiplier on the 'speed' of time. i.e. a value of 0.5 means time elapsed half as fast for this clock """ raise ValueError( @@ -150,7 +150,7 @@ def tick(self, delta_time: float): Update the clock with the time that has passed since the last tick. Args: - delta_time (float): The amount of time that has passed since the last tick. + delta_time: The amount of time that has passed since the last tick. """ if delta_time != self._fixed_rate: raise ValueError( @@ -184,11 +184,11 @@ def _setup_clock(initial_elapsed: float = 0.0, initial_tick: int = 0, tick_speed Private method used by the arcade window to setup the global clock post initialization. Args: - initial_elapsed (float, optional): The amount of time the clock should assume + initial_elapsed: The amount of time the clock should assume has already occurred. Defaults to 0.0 - initial_tick (int, optional): The number of ticks the clock should assume has + initial_tick: The number of ticks the clock should assume has already occurred. Defaults to 0. - tick_speed (float, optional): A multiplier on the 'speed' of time. + tick_speed: A multiplier on the 'speed' of time. i.e. a value of 0.5 means time elapsed half as fast for this clock. Defaults to 1.0. """ @@ -203,7 +203,7 @@ def _setup_fixed_clock(fixed_tick_rate: float = 1.0 / 60.0): post initialization. Args: - fixed_tick_rate (float, optional): The fixed number of seconds that pass + fixed_tick_rate: The fixed number of seconds that pass for this clock every tick. Defaults to 1.0 / 60.0 """ GLOBAL_FIXED_CLOCK._elapsed_time = GLOBAL_CLOCK.time # noqa: SLF001 diff --git a/arcade/examples/gui/0_basic_setup.py b/arcade/examples/gui/0_basic_setup.py new file mode 100644 index 0000000000..362d3908d3 --- /dev/null +++ b/arcade/examples/gui/0_basic_setup.py @@ -0,0 +1,111 @@ +"""Demonstrates general setup. + +If arcade and Python are properly installed, you can run this example with: +python -m arcade.examples.gui.0_basic_setup + +Content: +- create a view manually creating UIManager and adding widgets +- create a second view extending UIView and adding widgets + +""" + +import arcade +from arcade.gui import ( + UIManager, + UITextureButton, + UIAnchorLayout, +) + +# Preload textures, because they are mostly used multiple times, so they are not +# loaded multiple times +TEX_RED_BUTTON_NORMAL = arcade.load_texture(":resources:gui_basic_assets/button/red_normal.png") +TEX_RED_BUTTON_HOVER = arcade.load_texture(":resources:gui_basic_assets/button/red_hover.png") +TEX_RED_BUTTON_PRESS = arcade.load_texture(":resources:gui_basic_assets/button/red_press.png") + + +class GreenView(arcade.View): + """Uses the arcade.View and shows how to integrate UIManager.""" + + def __init__(self): + super().__init__() + + # Create a UIManager + self.ui = UIManager() + + # Create a anchor layout, which can be used to position widgets on screen + anchor = self.ui.add(UIAnchorLayout()) + + # Add a button switch to the other View. + button = anchor.add( + UITextureButton( + text="Switch to blue view", + texture=TEX_RED_BUTTON_NORMAL, + texture_hovered=TEX_RED_BUTTON_HOVER, + texture_pressed=TEX_RED_BUTTON_PRESS, + on_click=lambda: self.window.show_view(self.window.views["other"]), + ) + ) + + # add a button to switch to the blue view + @button.event("on_click") + def on_click(event): + self.window.show_view(BlueView()) + + def on_show_view(self) -> None: + self.ui.enable() + + def on_hide_view(self) -> None: + self.ui.disable() + + def on_draw(self): + # Clear the screen + self.clear(color=arcade.uicolor.GREEN_EMERALD) + + # Add draw commands that should be below the UI + # ... + + self.ui.draw() + + # Add draw commands that should be on top of the UI (uncommon) + # ... + + +class BlueView(arcade.gui.UIView): + """Uses the arcade.gui.UIView which takes care about the UIManager setup.""" + + def __init__(self): + super().__init__() + self.background_color = arcade.uicolor.BLUE_PETER_RIVER + + # Create a anchor layout, which can be used to position widgets on screen + anchor = self.add_widget(UIAnchorLayout()) + + # Add a button switch to the other View. + button = anchor.add( + UITextureButton( + text="Switch to green view", + texture=TEX_RED_BUTTON_NORMAL, + texture_hovered=TEX_RED_BUTTON_HOVER, + texture_pressed=TEX_RED_BUTTON_PRESS, + on_click=lambda: self.window.show_view(self.window.views["my"]), + ) + ) + + # add a button to switch to the green view + @button.event("on_click") + def on_click(event): + self.window.show_view(GreenView()) + + def on_draw_before_ui(self): + # Add draw commands that should be below the UI + pass + + def on_draw_after_ui(self): + # Add draw commands that should be on top of the UI (uncommon) + pass + + +if __name__ == "__main__": + window = arcade.Window(title="GUI Example: Basic Setup") + window.show_view(GreenView()) + window.run() diff --git a/arcade/examples/gui/1_layouts.py b/arcade/examples/gui/1_layouts.py new file mode 100644 index 0000000000..3d77f80487 --- /dev/null +++ b/arcade/examples/gui/1_layouts.py @@ -0,0 +1,156 @@ +"""Demonstrates the use of layouts. + +If arcade and Python are properly installed, you can run this example with: +python -m arcade.examples.gui.1_layouts + +Content: +- Create a view with a description of layouts +- Use UIAnchorLayout to position widgets relative to the screen edges +- Use UIBoxLayout to position widgets in a horizontal or vertical line +- Use UIGridLayout to position widgets in a grid + +""" + +from datetime import datetime + +import arcade +from arcade.gui import UIAnchorLayout + +arcade.resources.load_system_fonts() + +DESCRIPTION = """How to place widgets on the screen? + +Widgets can be manually placed on the screen by using the `add` method of the UIManager +and setting the rect of a widget. This is useful for simple ui setups. + +This approach requires to manually calculate the position of the +widgets and handling screen resizes. + +Another approach is to use layouts. Layouts are containers that automatically +position widgets based on the layout rules. This example shows how to use +the UIAnchorLayout, UIBoxLayout, and UIGridLayout. + +UIAnchorLayout: +- UIAnchorLayout is a layout that positions widgets relative to the screen edges. +- Widgets can be anchored to the top, bottom, left, right, center, or any combination of these. +- Widgets can be aligned to the anchor position with a pixel offset. + +UIBoxLayout: +- UIBoxLayout is a layout that positions widgets in a horizontal or vertical line. +- Widgets can be aligned on the orthogonal axis. +- Widgets can have a space between them. + +UIGridLayout: +- UIGridLayout is a layout that positions widgets in a grid. +- Widgets can be placed in rows and columns. +- Widgets can have a vertical and horizontal spacing. + +The layouts calculate the size of the widgets based on the size_hint, +size_hint_min, and size_hint_max. + +- size_hint: A tuple of two floats that represent the relative size of the widget. +- size_hint_min: A tuple of two floats that represent the minimum size of the widget. +- size_hint_max: A tuple of two floats that represent the maximum size of the widget. + +Layouts only resize widgets if the size_hint is set for the resprecitve axis. +If the size_hint is not set, the widget will use its current size. + +Some widgets calculate their minimum size based on their content like UILabel +and layouts in general. +""" + + +class LayoutView(arcade.gui.UIView): + """This view demonstrates the use of layouts.""" + + def __init__(self): + super().__init__() + self.background_color = arcade.uicolor.BLUE_PETER_RIVER + + # Create a anchor layout, which can be used to position widgets on screen + self.anchor = self.add_widget(UIAnchorLayout()) + + # Add describing text in center + text_area = arcade.gui.UITextArea( + text=DESCRIPTION, + text_color=arcade.uicolor.WHITE_CLOUDS, + font_name=("Lato", "proxima-nova", "Helvetica Neue", "Arial", "sans-serif"), + font_size=12, + size_hint=(0.5, 0.8), + ) + self.anchor.add(text_area, anchor_x="center_x", anchor_y="center_y") + text_area.with_border(color=arcade.uicolor.GRAY_CONCRETE) + text_area.with_background(color=arcade.uicolor.GRAY_CONCRETE.replace(a=125)) + text_area.with_padding(left=5) + + # add a grid layout with the window and grid size and grid position + self.grid = arcade.gui.UIGridLayout( + column_count=2, + row_count=2, + align_horizontal="left", + ) + self.grid.with_background(color=arcade.uicolor.GRAY_CONCRETE) + self.grid.with_border(color=arcade.uicolor.GRAY_ASBESTOS) + self.grid.with_padding(all=10) + self.anchor.add(self.grid, anchor_x="left", anchor_y="top", align_x=10, align_y=-10) + self.grid.add( + arcade.gui.UILabel(text="Arcade: "), + row=0, + column=0, + ) + self.grid.add( + arcade.gui.UILabel(text=arcade.VERSION), + row=0, + column=1, + ) + self.grid.add( + arcade.gui.UILabel(text="Today: "), + row=1, + column=0, + ) + self.grid.add( + arcade.gui.UILabel(text=datetime.utcnow().isoformat()[:10]), + row=1, + column=1, + ) + + # add a horizontal boxlayout with buttons + h_box = arcade.gui.UIBoxLayout(space_between=20, vertical=False) + self.anchor.add(h_box, anchor_x="center_x", anchor_y="bottom", align_y=5) + + # Add a button to move the grid layout to top_right + # top left + move_left_button = arcade.gui.UIFlatButton(text="Top Left", width=150) + move_left_button.disabled = True + h_box.add(move_left_button) + + @move_left_button.event("on_click") + def _(event): + self.anchor.remove(self.grid) + self.anchor.add(self.grid, anchor_x="left", anchor_y="top", align_x=10, align_y=-10) + move_left_button.disabled = True + move_right_button.disabled = False + + move_right_button = arcade.gui.UIFlatButton(text="Top Right", width=150) + h_box.add(move_right_button) + + @move_right_button.event("on_click") + def _(event): + self.anchor.remove(self.grid) + self.anchor.add(self.grid, anchor_x="right", anchor_y="top", align_x=-10, align_y=-10) + move_right_button.disabled = True + move_left_button.disabled = False + + def on_draw_before_ui(self): + # Add draw commands that should be below the UI + pass + + def on_draw_after_ui(self): + # Add draw commands that should be on top of the UI (uncommon) + pass + + +if __name__ == "__main__": + window = arcade.Window(title="GUI Example: Layouts") + window.show_view(LayoutView()) + window.run() diff --git a/arcade/examples/gui/2_widgets.py b/arcade/examples/gui/2_widgets.py new file mode 100644 index 0000000000..4bfb6a6246 --- /dev/null +++ b/arcade/examples/gui/2_widgets.py @@ -0,0 +1,608 @@ +"""An overview of all included widgets. + +See the other GUI examples for more indepth information about specific widgets. + +If arcade and Python are properly installed, you can run this example with: +python -m arcade.examples.gui.2_widgets +""" + +from __future__ import annotations + +import textwrap +from copy import deepcopy + +import arcade.gui +from arcade import TextureAnimation, TextureKeyframe, load_texture +from arcade import uicolor +from arcade.gui import ( + UIAnchorLayout, + UIButtonRow, + UIFlatButton, + UILabel, + UISpace, + UIOnActionEvent, + UITextArea, + UIOnChangeEvent, + UITextureButton, + UITextureToggle, + UISlider, + UITextureSlider, + UIBoxLayout, + UIImage, + UIDummy, + UISpriteWidget, + NinePatchTexture, + UIDropdown, + UIMessageBox, + UIManager, +) + +# Load system fonts +arcade.resources.load_system_fonts() + +DEFAULT_FONT = ("Kenney Future", "arial") +DETAILS_FONT = ("arial", "Kenney Future Narrow") + +# Preload textures, because they are mostly used multiple times, so they are not +# loaded multiple times +TEX_RED_BUTTON_NORMAL = load_texture(":resources:gui_basic_assets/button/red_normal.png") +TEX_RED_BUTTON_HOVER = load_texture(":resources:gui_basic_assets/button/red_hover.png") +TEX_RED_BUTTON_PRESS = load_texture(":resources:gui_basic_assets/button/red_press.png") +TEX_RED_BUTTON_DISABLE = load_texture(":resources:gui_basic_assets/button/red_disabled.png") + +TEX_TOGGLE_RED = load_texture(":resources:gui_basic_assets/toggle/red.png") +TEX_TOGGLE_GREEN = load_texture(":resources:gui_basic_assets/toggle/green.png") + +TEX_CHECKBOX_CHECKED = load_texture(":resources:gui_basic_assets/checkbox/blue_check.png") +TEX_CHECKBOX_UNCHECKED = load_texture(":resources:gui_basic_assets/checkbox/empty.png") + +TEX_SLIDER_THUMB_BLUE = load_texture(":resources:gui_basic_assets/slider/thumb_blue.png") +TEX_SLIDER_TRACK_BLUE = load_texture(":resources:gui_basic_assets/slider/track_blue.png") +TEX_SLIDER_THUMB_RED = load_texture(":resources:gui_basic_assets/slider/thumb_red.png") +TEX_SLIDER_TRACK_RED = load_texture(":resources:gui_basic_assets/slider/track_red.png") +TEX_SLIDER_THUMB_GREEN = load_texture(":resources:gui_basic_assets/slider/thumb_green.png") +TEX_SLIDER_TRACK_GREEN = load_texture(":resources:gui_basic_assets/slider/track_green.png") + +TEX_NINEPATCH_BASE = load_texture(":resources:gui_basic_assets/window/grey_panel.png") + +TEX_ARCADE_LOGO = load_texture(":resources:/logo.png") + +# Load animation for the sprite widget +frame_textures = [] +for i in range(8): + tex = load_texture( + f":resources:images/animated_characters/female_adventurer/femaleAdventurer_walk{i}.png" + ) + frame_textures.append(tex) + +TEX_ANIMATED_CHARACTER = TextureAnimation([TextureKeyframe(frame) for frame in frame_textures]) + +TEXT_WIDGET_EXPLANATION = textwrap.dedent(""" +Arcade GUI provides three types of text widgets: + + +- {bold True}UILabel{bold False}: +A simple text widget that can be used to display text. + +- {bold True}UIInputText{bold False}: +A text widget that can be used to get text input from the user. + +- {bold True}UITextArea{bold False}: +A text widget that can be used to display text that is too long for a label. + + +This example shows how to use all three types of text widgets. + + +A few hints regarding the usage of the text widgets: + + +//Please scroll ... + + +{bold True}UILabel{bold False}: + +If you want to display frequently changing text, +setting a background color will improve performance. + + +{bold True}UIInputText{bold False}: + +UIInputText dispatches an event on_change, when the text changes. + + +{bold True}UITextArea{bold False}: + +While the widget supports scrolling, there is no scrollbar provided yet. +Users might oversee content. + +In addition UITextArea supports different text styles, +which relate to Pyglet FormattedDocument. + +"PLAIN" - Plain text. + +"ATTRIBUTED" - Attributed text following the Pyglet attributed text style. + +"HTML" - Allows to use HTML tags for formatting. + +""").strip() + + +class GalleryView(arcade.gui.UIView): + def __init__(self): + super().__init__() + self.background_color = uicolor.BLUE_BELIZE_HOLE + + root = self.add_widget(UIAnchorLayout()) + + # Setup side navigation + nav_side = UIButtonRow(vertical=True, size_hint=(0.3, 1)) + nav_side.add( + UILabel( + "Categories", + font_name=DEFAULT_FONT, + font_size=32, + text_color=uicolor.DARK_BLUE_MIDNIGHT_BLUE, + size_hint=(1, 0.1), + align="center", + ) + ) + nav_side.add(UISpace(size_hint=(1, 0.01), color=uicolor.DARK_BLUE_MIDNIGHT_BLUE)) + + nav_side.with_padding(all=10) + nav_side.with_background(color=uicolor.WHITE_CLOUDS) + nav_side.add_button("Start", style=UIFlatButton.STYLE_BLUE, size_hint=(1, 0.1)) + nav_side.add_button("Text", style=UIFlatButton.STYLE_BLUE, size_hint=(1, 0.1)) + nav_side.add_button("Interactive", style=UIFlatButton.STYLE_BLUE, size_hint=(1, 0.1)) + nav_side.add_button("Constructs", style=UIFlatButton.STYLE_BLUE, size_hint=(1, 0.1)) + nav_side.add_button("Other", style=UIFlatButton.STYLE_BLUE, size_hint=(1, 0.1)) + root.add(nav_side, anchor_x="left", anchor_y="top") + + @nav_side.event("on_action") + def on_action(event: UIOnActionEvent): + if event.action == "Start": + self._show_start_widgets() + elif event.action == "Text": + self._show_text_widgets() + elif event.action == "Interactive": + self._show_interactive_widgets() + elif event.action == "Constructs": + self._show_construct_widgets() + elif event.action == "Other": + self._show_other_widgets() + + # Setup content to show widgets in + + self._body = UIAnchorLayout(size_hint=(0.7, 1)) + self._body.with_padding(all=20) + root.add(self._body, anchor_x="right", anchor_y="top") + + # init start widgets + self._show_start_widgets() + + def _show_start_widgets(self): + """Show a short introduction message.""" + self._body.clear() + self._body.add( + UITextArea( + text=textwrap.dedent(""" + Welcome to the Widget Gallery + + Choose a category on the left to see the widgets. + You can checkout the source code in the examples/gui folder + to see how this gallery is built. + """).strip(), + font_name=DETAILS_FONT, + font_size=32, + text_color=uicolor.WHITE_CLOUDS, + size_hint=(0.8, 0.8), + ), + anchor_y="top", + ) + open_sourcecode = self._body.add( + UIFlatButton( + text="Open Source Code", style=UIFlatButton.STYLE_RED, size_hint=(0.3, 0.1) + ), + anchor_y="bottom", + align_y=20, + ) + + @open_sourcecode.event("on_click") + def on_click(_): + """This will open the source code of this example on GitHub.""" + import webbrowser + + webbrowser.open( + "https://github.com/pythonarcade/arcade/tree/development/arcade/examples/gui" + ) + + def _show_text_widgets(self): + """Show the text widgets. + + For this we clear the body and add the text widgets. + + Featuring: + - UILabel + - UIInputText + - UITextArea + """ + + self._body.clear() + + box = arcade.gui.UIBoxLayout(vertical=True, size_hint=(1, 1), align="left") + self._body.add(box) + box.add(UILabel("Text Widgets", font_name=DEFAULT_FONT, font_size=32)) + box.add(UISpace(size_hint=(1, 0.1))) + + row_1 = arcade.gui.UIBoxLayout(vertical=False, size_hint=(1, 0.1)) + box.add(row_1) + row_1.add(UILabel("Name: ", font_name=DEFAULT_FONT, font_size=24)) + name_input = row_1.add( + arcade.gui.UIInputText( + width=400, + height=40, + font_name=DEFAULT_FONT, + font_size=24, + border_color=uicolor.GRAY_CONCRETE, + border_width=2, + ) + ) + welcome_label = box.add( + UILabel("Nice to meet you ''", font_name=DEFAULT_FONT, font_size=24) + ) + + @name_input.event("on_change") + def on_text_change(event: UIOnChangeEvent): + welcome_label.text = f"Nice to meet you `{event.new_value}`" + + box.add(UISpace(size_hint=(1, 0.3))) # Fill some of the left space + + text_area = box.add( + UITextArea( + text=TEXT_WIDGET_EXPLANATION, + size_hint=(1, 0.9), + font_name=DETAILS_FONT, + font_size=16, + text_color=uicolor.WHITE_CLOUDS, + document_mode="ATTRIBUTED", + ) + ) + text_area.with_padding(left=10, right=10) + text_area.with_border(color=uicolor.GRAY_CONCRETE, width=2) + + def _show_interactive_widgets(self): + self._body.clear() + box = arcade.gui.UIBoxLayout( + vertical=True, size_hint=(1, 1), align="left", space_between=10 + ) + self._body.add(box) + box.add(UILabel("Interactive Widgets", font_name=DEFAULT_FONT, font_size=32)) + box.add(UISpace(size_hint=(1, 0.1))) + + flat_row = arcade.gui.UIBoxLayout(vertical=False, size_hint=(1, 0.1), space_between=10) + box.add(flat_row) + + flat_row.add( + UIFlatButton( + text="UIFlatButton blue", style=UIFlatButton.STYLE_BLUE, size_hint=(0.3, 1) + ) + ) + flat_row.add( + UIFlatButton( + text="UIFlatButton red", style=UIFlatButton.STYLE_RED, size_hint=(0.3, 1) + ) + ) + flat_row.add( + UIFlatButton(text="disabled", style=UIFlatButton.STYLE_BLUE, size_hint=(0.3, 1)) + ).disabled = True + + tex_row = arcade.gui.UIBoxLayout(vertical=False, size_hint=(1, 0.1), space_between=10) + box.add(tex_row) + tex_row.add( + UITextureButton( + text="UITextureButton", + texture=TEX_RED_BUTTON_NORMAL, + texture_hovered=TEX_RED_BUTTON_HOVER, + texture_pressed=TEX_RED_BUTTON_PRESS, + texture_disabled=TEX_RED_BUTTON_DISABLE, + size_hint=(0.3, 1), + ) + ) + + tex_row.add(UISpace(size_hint=(0.3, 1))) + + tex_row.add( + UITextureButton( + text="disabled", + texture=TEX_RED_BUTTON_NORMAL, + texture_hovered=TEX_RED_BUTTON_HOVER, + texture_pressed=TEX_RED_BUTTON_PRESS, + texture_disabled=TEX_RED_BUTTON_DISABLE, + size_hint=(0.3, 1), + ) + ).disabled = True + + toggle_row = arcade.gui.UIBoxLayout(vertical=False, size_hint=(1, 0.1), space_between=10) + box.add(toggle_row) + toggle_row.add( + UILabel("UITextureToggle", font_name=DETAILS_FONT, font_size=16, size_hint=(0.3, 0)) + ) + toggle_row.add( + UITextureToggle( + on_texture=TEX_TOGGLE_RED, + off_texture=TEX_TOGGLE_GREEN, + width=64, + height=32, + ) + ) + toggle_row.add( + UITextureToggle( + on_texture=TEX_CHECKBOX_CHECKED, + off_texture=TEX_CHECKBOX_UNCHECKED, + width=32, + height=32, + ) + ) + + dropdown_row = arcade.gui.UIBoxLayout( + vertical=False, size_hint=(1, 0.1), space_between=10 + ) + box.add(dropdown_row) + dropdown_row.add( + UILabel("UIDropdown", font_name=DETAILS_FONT, font_size=16, size_hint=(0.3, 0)) + ) + dropdown_row.add( + UIDropdown( + default="Option 1", + options=["Option 1", "Option 2", "Option 3"], + ) + ) + + slider_row = arcade.gui.UIBoxLayout(vertical=False, size_hint=(1, 0.1), space_between=10) + box.add(slider_row) + + slider_row.add( + UILabel( + "UISlider", + font_name=DETAILS_FONT, + font_size=16, + size_hint=(0.3, 0), + ) + ) + slider_row.add( + UISlider( + size_hint=(0.2, None), + ) + ) + + tex_slider_row = arcade.gui.UIBoxLayout( + vertical=False, size_hint=(1, 0.1), space_between=10 + ) + box.add(tex_slider_row) + + tex_slider_row.add( + UILabel( + "UITextureSlider", + font_name=DETAILS_FONT, + font_size=16, + size_hint=(0.3, 0), + ) + ) + + s1 = tex_slider_row.add( + UITextureSlider( + thumb_texture=TEX_SLIDER_THUMB_BLUE, + track_texture=NinePatchTexture(10, 10, 10, 10, TEX_SLIDER_TRACK_BLUE), + size_hint=(0.3, None), + ) + ) + + green_style = deepcopy(UITextureSlider.DEFAULT_STYLE) + green_style["normal"].filled_track = uicolor.GREEN_GREEN_SEA + green_style["hover"].filled_track = uicolor.GREEN_EMERALD + green_style["press"].filled_track = uicolor.GREEN_GREEN_SEA + s2 = tex_slider_row.add( + UITextureSlider( + thumb_texture=TEX_SLIDER_THUMB_GREEN, + track_texture=NinePatchTexture(10, 10, 10, 10, TEX_SLIDER_TRACK_GREEN), + size_hint=(0.3, None), + style=green_style, + ) + ) + + red_style = deepcopy(UITextureSlider.DEFAULT_STYLE) + red_style["normal"].filled_track = uicolor.RED_POMEGRANATE + red_style["hover"].filled_track = uicolor.RED_ALIZARIN + red_style["press"].filled_track = uicolor.RED_POMEGRANATE + s3 = tex_slider_row.add( + UITextureSlider( + thumb_texture=TEX_SLIDER_THUMB_RED, + track_texture=NinePatchTexture(10, 10, 10, 10, TEX_SLIDER_TRACK_RED), + size_hint=(0.3, None), + style=red_style, + ) + ) + + @s1.event("on_change") + def _(event: UIOnChangeEvent): + s2.value = event.new_value + s3.value = event.new_value + + @s2.event("on_change") + def _(event: UIOnChangeEvent): + s1.value = event.new_value + s3.value = event.new_value + + @s3.event("on_change") + def _(event: UIOnChangeEvent): + s1.value = event.new_value + s2.value = event.new_value + + box.add(UISpace(size_hint=(0.2, 0.1))) + text_area = box.add( + UITextArea( + text=textwrap.dedent(""" + Interactive widgets are widgets that the user can interact with. + This includes buttons, toggles, sliders and more. + + By subclassing UIInteractiveWidget you + can create your own interactive widgets. + + For text input have a look at the text widgets. + """).strip(), + font_name=DETAILS_FONT, + font_size=16, + text_color=uicolor.WHITE_CLOUDS, + size_hint=(1, 0.9), + ) + ) + text_area.with_padding(left=10, right=10) + text_area.with_border(color=uicolor.GRAY_CONCRETE, width=2) + + def _show_construct_widgets(self): + self._body.clear() + box = arcade.gui.UIBoxLayout( + vertical=True, size_hint=(1, 1), align="left", space_between=10 + ) + self._body.add(box) + box.add(UILabel("Constructs", font_name=DEFAULT_FONT, font_size=32)) + box.add(UISpace(size_hint=(1, 0.1))) + + message_row = arcade.gui.UIBoxLayout( + vertical=False, size_hint=(1, 0.1), space_between=10 + ) + box.add(message_row) + message_row.add( + UILabel( + "UIMessageBox", + font_name=DETAILS_FONT, + font_size=16, + size_hint=(0.3, 0), + ) + ) + message_button = message_row.add( + UIFlatButton( + text="Show Message", + style=UIFlatButton.STYLE_BLUE, + size_hint=(0.3, 1), + ) + ) + + @message_button.event("on_click") + def on_click(event): + self.ui.add( + UIMessageBox( + width=500, + height=350, + title="Message Box", + buttons=("Ok", "Cancel"), + message_text=textwrap.dedent(""" + This is a message box. + It can be used to show messages to the user. + + You can add buttons to it, to let the user choose an action. + """).strip(), + ), + layer=UIManager.OVERLAY_LAYER, + ) + + button_row = arcade.gui.UIBoxLayout(vertical=False, size_hint=(1, 0.1), space_between=10) + box.add(button_row) + button_row.add( + UILabel( + "UIButtonRow", + font_name=DETAILS_FONT, + font_size=16, + size_hint=(0.3, 0), + ) + ) + buttons = button_row.add( + UIButtonRow( + text="Show Message", + style=UIFlatButton.STYLE_BLUE, + size_hint=(1, 0), + ) + ) + buttons.add_button("Default Style", size_hint=(1, None)) + buttons.add_button("Red Style", style=UIFlatButton.STYLE_RED, size_hint=(1, None)) + buttons.add_button("Blue Style", style=UIFlatButton.STYLE_BLUE, size_hint=(1, None)) + + # Constructs + # "UIButtonRow", + + box.add(UISpace(size_hint=(0.2, 0.1))) + text_area = box.add( + UITextArea( + text=textwrap.dedent(""" + Constructs are widgets that combine multiple widgets, to provide common functionality + within a simple widget. + Examples for this are message boxes or rows of buttons. + """).strip(), + font_name=DETAILS_FONT, + font_size=16, + text_color=uicolor.WHITE_CLOUDS, + size_hint=(1, 0.5), + ) + ) + text_area.with_padding(left=10, right=10) + text_area.with_border(color=uicolor.GRAY_CONCRETE, width=2) + + def _show_other_widgets(self): + self._body.clear() + box = arcade.gui.UIBoxLayout( + vertical=True, size_hint=(1, 1), align="left", space_between=10 + ) + self._body.add(box) + box.add(UILabel("Other Widgets", font_name=DEFAULT_FONT, font_size=32)) + box.add(UISpace(size_hint=(1, 0.1))) + + image_row = box.add(UIBoxLayout(vertical=False, size_hint=(1, 0.1))) + image_row.add( + UILabel("UIImage", font_name=DETAILS_FONT, font_size=16, size_hint=(0.3, 0)) + ) + image_row.add(UIImage(texture=TEX_ARCADE_LOGO, width=64, height=64)) + + dummy_row = box.add(UIBoxLayout(vertical=False, size_hint=(1, 0.1))) + dummy_row.add( + UILabel("UIDummy", font_name=DETAILS_FONT, font_size=16, size_hint=(0.3, 0)) + ) + dummy_row.add(UIDummy(size_hint=(0.2, 1))) + dummy_row.add(UIDummy(size_hint=(0.2, 1))) + dummy_row.add(UIDummy(size_hint=(0.2, 1))) + + sprite = arcade.TextureAnimationSprite(animation=TEX_ANIMATED_CHARACTER) + sprite.scale = 0.5 + sprite_row = box.add(UIBoxLayout(vertical=False, size_hint=(1, 0.1))) + sprite_row.add( + UILabel("UISpriteWidget", font_name=DETAILS_FONT, font_size=16, size_hint=(0.3, 0)) + ) + sprite_row.add(UISpriteWidget(sprite=sprite, width=sprite.width, height=sprite.height)) + + box.add(UISpace(size_hint=(0.2, 0.1))) + text_area = box.add( + UITextArea( + text=textwrap.dedent(""" + Arcade GUI provides also a few more widgets for special use cases. + + - UIImage: A widget to display an image. + - UIDummy: Which can be used as a placeholder. + It renders a random color and changes it on click. + - UISpace: A widget that only takes up space. + But can also be used to add a colored space. + - UISpriteWidget: A widget that can display a sprite. + + """).strip(), + font_name=DETAILS_FONT, + font_size=16, + text_color=uicolor.WHITE_CLOUDS, + size_hint=(1, 0.9), + ) + ) + text_area.with_padding(left=10, right=10) + text_area.with_border(color=uicolor.GRAY_CONCRETE, width=2) + + +if __name__ == "__main__": + window = arcade.Window(title="GUI Example: Widget Gallery") + window.show_view(GalleryView()) + window.run() diff --git a/arcade/gui/examples/button_with_text.py b/arcade/examples/gui/3_buttons.py similarity index 87% rename from arcade/gui/examples/button_with_text.py rename to arcade/examples/gui/3_buttons.py index 979ea549e1..594a78a817 100644 --- a/arcade/gui/examples/button_with_text.py +++ b/arcade/examples/gui/3_buttons.py @@ -5,7 +5,7 @@ arrange buttons. If arcade and Python are properly installed, you can run this example with: -python -m arcade.gui.examples.button_with_text +python -m arcade.examples.gui.3_buttons """ from __future__ import annotations @@ -23,11 +23,11 @@ ICON_SMALLER = load_texture(":resources:gui_basic_assets/icons/smaller.png") ICON_LARGER = load_texture(":resources:gui_basic_assets/icons/larger.png") -TEX_SWITCH_GREEN = load_texture(":resources:gui_basic_assets/toggle/switch_green.png") -TEX_SWITCH_RED = load_texture(":resources:gui_basic_assets/toggle/switch_red.png") -TEX_RED_BUTTON_NORMAL = load_texture(":resources:gui_basic_assets/red_button_normal.png") -TEX_RED_BUTTON_HOVER = load_texture(":resources:gui_basic_assets/red_button_hover.png") -TEX_RED_BUTTON_PRESS = load_texture(":resources:gui_basic_assets/red_button_press.png") +TEX_SWITCH_GREEN = load_texture(":resources:gui_basic_assets/toggle/green.png") +TEX_SWITCH_RED = load_texture(":resources:gui_basic_assets/toggle/red.png") +TEX_RED_BUTTON_NORMAL = load_texture(":resources:gui_basic_assets/button/red_normal.png") +TEX_RED_BUTTON_HOVER = load_texture(":resources:gui_basic_assets/button/red_hover.png") +TEX_RED_BUTTON_PRESS = load_texture(":resources:gui_basic_assets/button/red_press.png") class MyView(arcade.View): @@ -46,17 +46,17 @@ def __init__(self): self.ui.add(UIAnchorLayout(children=[grid])) # simple UIFlatButton with text - grid.add(UIFlatButton(text="UIFlatButton", width=200), row_num=0, col_num=0) + grid.add(UIFlatButton(text="UIFlatButton", width=200), row=0, column=0) # UIFlatButton change text placement right flat_with_more_text = UIFlatButton(text="text placed right", width=200) flat_with_more_text.place_text(anchor_x="right") - grid.add(flat_with_more_text, row_num=1, col_num=0) + grid.add(flat_with_more_text, row=1, column=0) # UIFlatButton change text placement right flat_with_more_text = UIFlatButton(text="text placed top left", width=200) flat_with_more_text.place_text(anchor_x="left", anchor_y="top") - grid.add(flat_with_more_text, row_num=2, col_num=0) + grid.add(flat_with_more_text, row=2, column=0) # UIFlatButton with icon on the left flat_with_icon_left = UIFlatButton(text="UIFlatButton with icon", width=200) @@ -70,7 +70,7 @@ def __init__(self): anchor_x="left", align_x=10, ) - grid.add(flat_with_icon_left, row_num=0, col_num=1) + grid.add(flat_with_icon_left, row=0, column=1) # UIFlatButton with icon on the right flat_with_icon_right = UIFlatButton(text="UIFlatButton with icon", width=200) @@ -84,7 +84,7 @@ def __init__(self): anchor_x="right", align_x=-10, ) - grid.add(flat_with_icon_right, row_num=1, col_num=1) + grid.add(flat_with_icon_right, row=1, column=1) # UIFlatButton with icon on both sides flat_with_icon_right = UIFlatButton(text="UIFlatButton", width=200) @@ -106,7 +106,7 @@ def __init__(self): anchor_x="right", align_x=-10, ) - grid.add(flat_with_icon_right, row_num=2, col_num=1) + grid.add(flat_with_icon_right, row=2, column=1) # UITextureButton texture_button = UITextureButton( @@ -116,7 +116,7 @@ def __init__(self): texture_hovered=TEX_RED_BUTTON_HOVER, texture_pressed=TEX_RED_BUTTON_PRESS, ) - grid.add(texture_button, row_num=0, col_num=2) + grid.add(texture_button, row=0, column=2) # UITextureButton with icon left texture_button_with_icon_left = UITextureButton( @@ -135,7 +135,7 @@ def __init__(self): anchor_x="left", align_x=10, ) - grid.add(texture_button_with_icon_left, row_num=1, col_num=2) + grid.add(texture_button_with_icon_left, row=1, column=2) # UITextureButton with multiline text texture_button_with_icon_left = UITextureButton( @@ -156,7 +156,7 @@ def __init__(self): anchor_x="left", align_x=10, ) - grid.add(texture_button_with_icon_left, row_num=2, col_num=2) + grid.add(texture_button_with_icon_left, row=2, column=2) # UIFlatButtons with toggle texture_button_with_toggle = UIFlatButton( @@ -188,10 +188,10 @@ def __init__(self): def on_change(event: UIOnChangeEvent): texture_button_with_toggle.disabled = event.new_value - grid.add(texture_button_with_toggle, row_num=3, col_num=0, col_span=3) + grid.add(texture_button_with_toggle, row=3, column=0, column_span=3) def on_show_view(self): - self.window.background_color = arcade.color.DARK_BLUE_GRAY + self.window.background_color = arcade.uicolor.BLUE_BELIZE_HOLE # Enable UIManager when view is shown to catch window events self.ui.enable() @@ -205,6 +205,6 @@ def on_draw(self): if __name__ == "__main__": - window = arcade.Window(800, 600, "UIExample", resizable=True) + window = arcade.Window(800, 600, "GUI Example: Buttons", resizable=True) window.show_view(MyView()) window.run() diff --git a/arcade/gui/examples/gui_and_camera.py b/arcade/examples/gui/4_with_camera.py similarity index 93% rename from arcade/gui/examples/gui_and_camera.py rename to arcade/examples/gui/4_with_camera.py index cf1107c2f1..072465e2d2 100644 --- a/arcade/gui/examples/gui_and_camera.py +++ b/arcade/examples/gui/4_with_camera.py @@ -7,7 +7,7 @@ At the beginning of the game, the UI camera is used, to apply some animations. If arcade and Python are properly installed, you can run this example with: -python -m arcade.gui.examples.gui_and_camera +python -m arcade.examples.gui.4_with_camera """ from __future__ import annotations @@ -20,6 +20,9 @@ from arcade.gui import UIView, UIFlatButton, UIOnClickEvent, UILabel, UIBoxLayout from arcade.gui.widgets.layout import UIAnchorLayout +COIN_PNG = ":resources:images/items/coinGold.png" +ADV_PNG = ":resources:/images/animated_characters/female_adventurer/femaleAdventurer_idle.png" + class MyCoinGame(UIView): """Main view of the game. This class is a subclass of UIView, which provides @@ -34,6 +37,7 @@ class MyCoinGame(UIView): def __init__(self): super().__init__() + self.bg_color = arcade.uicolor.DARK_BLUE_MIDNIGHT_BLUE # basic camera setup self.keys = set() @@ -65,7 +69,7 @@ def __init__(self): self.sprites.append(self.game_area) self.player = arcade.Sprite( - ":resources:/images/animated_characters/female_adventurer/femaleAdventurer_idle.png", + ADV_PNG, scale=0.5, center_x=1280 / 2, center_y=720 / 2, @@ -76,7 +80,7 @@ def __init__(self): for i in range(12): # place coins in a circle around the player, radius =100 coin = arcade.Sprite( - ":resources:images/items/coinGold.png", + COIN_PNG, scale=0.5, center_x=1280 / 2 + 200 * math.cos(math.radians(i * 40)), center_y=720 / 2 + 200 * math.sin(math.radians(i * 40)), @@ -193,7 +197,7 @@ def on_update(self, delta_time: float) -> Optional[bool]: # spawn new coins if self._last_coin_spawn > self._coin_spawn_delay: coin = arcade.Sprite( - ":resources:images/items/coinGold.png", + COIN_PNG, scale=0.5, center_x=random.randint(0, 1280), center_y=random.randint(0, 720), @@ -233,9 +237,11 @@ def on_update(self, delta_time: float) -> Optional[bool]: # slide in the UI from bottom, until total time reaches 2 seconds progress = min(1.0, self._total_time / 2) - # Because we allow for camera rotation we have work on the center + # Because we allow for camera rotation we have work in the center # and not the edge because it behaves oddly otherwise - self.ui.camera.position = (self.window.center_x, 50 * (1 - progress) + self.window.center_y) + cam_pos_x = self.window.center_x + cam_pos_y = 50 * (1 - progress) + self.window.center_y + self.ui.camera.position = (cam_pos_x, cam_pos_y) return False @@ -256,7 +262,6 @@ def on_key_release(self, symbol: int, modifiers: int) -> Optional[bool]: if __name__ == "__main__": - window = arcade.Window(1280, 720, "CoinGame Example", resizable=False) - window.background_color = arcade.color.DARK_BLUE_GRAY + window = arcade.Window(1280, 720, "GUI Example: Coin Game (Camera)", resizable=False) window.show_view(MyCoinGame()) window.run() diff --git a/arcade/examples/gui/5_uicolor_picker.py b/arcade/examples/gui/5_uicolor_picker.py new file mode 100644 index 0000000000..d0b6a73093 --- /dev/null +++ b/arcade/examples/gui/5_uicolor_picker.py @@ -0,0 +1,193 @@ +"""Show all arcade.uicolors in a grid. + +Click on a color to select +it and copy the arcade reference to the clipboard. + +If arcade and Python are properly installed, you can run this example with: +python -m arcade.examples.gui.5_uicolor_picker + +""" + +from dataclasses import dataclass + +import arcade +from arcade.gui import ( + UIAnchorLayout, + UIEvent, + UIInteractiveWidget, + UITextWidget, +) + + +@dataclass +class ChooseColorEvent(UIEvent): + """Custom event, which is dispatched when a color button is clicked.""" + + color_name: str + color: arcade.color.Color + + +class Toast(arcade.gui.UILabel): + """Label which disappears after a certain time.""" + + def __init__(self, text: str, duration: float = 2.0, **kwargs): + super().__init__(**kwargs) + self.text = text + self.duration = duration + self.time = 0 + + def on_update(self, dt): + self.time += dt + + if self.time > self.duration: + self.parent.remove(self) + + +class ColorButton(UITextWidget, UIInteractiveWidget): + """Button which shows a color and color name and + emits a ChooseColorEvent event when clicked.""" + + def __init__( + self, + color_name: str, + color: arcade.color.Color, + **kwargs, + ): + super().__init__(text=color_name, **kwargs) + # set color and place text on the bottom + self.with_background(color=color) + self.place_text(anchor_y="bottom") + + # set font color based on background color + f = 2 if color_name.startswith("DARK") else 0.5 + self.ui_label.update_font( + font_color=arcade.color.Color(int(color[0] * f), int(color[1] * f), int(color[2] * f)) + ) + + # store color name and color for later reference + self._color_name = color_name + self._color = color + + # register custom event + self.register_event_type("on_choose_color") + + def on_update(self, dt): + """Update the button state. + + UIInteractiveWidget provides properties like hovered and pressed, + which can be used to highlight the button.""" + if self.pressed: + self.with_border(color=arcade.uicolor.WHITE_CLOUDS, width=3) + elif self.hovered: + self.with_border(color=arcade.uicolor.WHITE_CLOUDS, width=2) + else: + self.with_border(color=arcade.color.BLACK, width=1) + + def on_click(self, event) -> bool: + """Emit a ChooseColorEvent event when clicked.""" + self.dispatch_event( + "on_choose_color", ChooseColorEvent(self, self._color_name, self._color) + ) + return True + + def on_choose_color(self, event: ChooseColorEvent): + """ChooseColorEvent event handler, which can be overridden.""" + pass + + +class ColorView(arcade.gui.UIView): + """Uses the arcade.gui.UIView which takes care about the UIManager setup.""" + + def __init__(self): + super().__init__() + # Create an anchor layout, which can be used to position widgets on screen + self.root = self.add_widget(UIAnchorLayout()) + + # Define colors in grid order + self.colors = { + # row 0 + "GREEN_TURQUOISE": arcade.uicolor.GREEN_TURQUOISE, + "GREEN_EMERALD": arcade.uicolor.GREEN_EMERALD, + "BLUE_PETER_RIVER": arcade.uicolor.BLUE_PETER_RIVER, + "PURPLE_AMETHYST": arcade.uicolor.PURPLE_AMETHYST, + "DARK_BLUE_WET_ASPHALT": arcade.uicolor.DARK_BLUE_WET_ASPHALT, + # row 1 + "GREEN_GREEN_SEA": arcade.uicolor.GREEN_GREEN_SEA, + "GREEN_NEPHRITIS": arcade.uicolor.GREEN_NEPHRITIS, + "BLUE_BELIZE_HOLE": arcade.uicolor.BLUE_BELIZE_HOLE, + "PURPLE_WISTERIA": arcade.uicolor.PURPLE_WISTERIA, + "DARK_BLUE_MIDNIGHT_BLUE": arcade.uicolor.DARK_BLUE_MIDNIGHT_BLUE, + # row 2 + "YELLOW_SUN_FLOWER": arcade.uicolor.YELLOW_SUN_FLOWER, + "ORANGE_CARROT": arcade.uicolor.ORANGE_CARROT, + "RED_ALIZARIN": arcade.uicolor.RED_ALIZARIN, + "WHITE_CLOUDS": arcade.uicolor.WHITE_CLOUDS, + "GRAY_CONCRETE": arcade.uicolor.GRAY_CONCRETE, + # row 3 + "YELLOW_ORANGE": arcade.uicolor.YELLOW_ORANGE, + "ORANGE_PUMPKIN": arcade.uicolor.ORANGE_PUMPKIN, + "RED_POMEGRANATE": arcade.uicolor.RED_POMEGRANATE, + "WHITE_SILVER": arcade.uicolor.WHITE_SILVER, + "GRAY_ASBESTOS": arcade.uicolor.GRAY_ASBESTOS, + } + + # setup grid with colors + self.grid = self.root.add( + arcade.gui.UIGridLayout( + column_count=5, + row_count=4, + size_hint=(1, 1), + ) + ) + for i, (name, color) in enumerate(self.colors.items()): + button = self.root.add( + ColorButton( + color_name=name, + color=color, + size_hint=(1, 1), + ) + ) + self.grid.add(button, row=i // 5, column=i % 5) + + # connect event handler + button.on_choose_color = self.on_color_button_choose_color + + # setup toasts (temporary messages) + self.toasts = self.root.add( + arcade.gui.UIBoxLayout(space_between=2), anchor_x="right", anchor_y="top" + ) + self.toasts.with_padding(all=10) + + def on_color_button_choose_color(self, event: ChooseColorEvent) -> bool: + """Color button click event handler, which copies the color name to the clipboard. + + And shows a temporary message.""" + self.window.set_clipboard_text(f"arcade.uicolor.{event.color_name}") + + # prepare and show toast + toast = Toast(f"Copied {event.color_name}", width=250, size_hint=(None, 0)) + toast.update_font( + font_color=arcade.uicolor.DARK_BLUE_MIDNIGHT_BLUE, + font_size=9, + bold=True, + ) + toast.with_background(color=arcade.uicolor.GREEN_EMERALD) + toast.with_padding(all=10) + + self.toasts.add(toast) + + return True + + def on_draw_before_ui(self): + # Add draw commands that should be below the UI + pass + + def on_draw_after_ui(self): + # Add draw commands that should be on top of the UI (uncommon) + pass + + +if __name__ == "__main__": + window = arcade.Window(title="GUI Example: Color Picker") + window.show_view(ColorView()) + window.run() diff --git a/arcade/gui/examples/size_hints.py b/arcade/examples/gui/6_size_hints.py similarity index 97% rename from arcade/gui/examples/size_hints.py rename to arcade/examples/gui/6_size_hints.py index 7453609dac..11cfe494b6 100644 --- a/arcade/gui/examples/size_hints.py +++ b/arcade/examples/gui/6_size_hints.py @@ -14,7 +14,7 @@ always be set. If arcade and Python are properly installed, you can run this example with: -python -m arcade.gui.examples.size_hints +python -m arcade.examples.gui.6_size_hints """ from __future__ import annotations diff --git a/arcade/gui/examples/dropdown.py b/arcade/examples/gui/dropdown.py similarity index 97% rename from arcade/gui/examples/dropdown.py rename to arcade/examples/gui/dropdown.py index c8ee0728b2..672b189ea0 100644 --- a/arcade/gui/examples/dropdown.py +++ b/arcade/examples/gui/dropdown.py @@ -4,7 +4,7 @@ by changing the text displayed on screen to reflect it. If arcade and Python are properly installed, you can run this example with: -python -m arcade.gui.examples.dropdown +python -m arcade.examples.gui.dropdown """ from __future__ import annotations diff --git a/arcade/examples/gui/exp_hidden_password.py b/arcade/examples/gui/exp_hidden_password.py new file mode 100644 index 0000000000..1d0b9bba8b --- /dev/null +++ b/arcade/examples/gui/exp_hidden_password.py @@ -0,0 +1,117 @@ +"""Creating a hidden password field + +This example demonstrates how to create a custom text input +which hides the contents behind a custom character, as often +required for login screens. + +Due to a bug in the current version of pyglet, the example uses ENTER to switch +fields instead of TAB. This will be fixed in future versions. +(https://github.com/pyglet/pyglet/issues/1197) + +If arcade and Python are properly installed, you can run this example with: +python -m arcade.examples.gui.exp_hidden_password +""" + +from __future__ import annotations + +import arcade +from arcade.gui import UIInputText, UIOnClickEvent, UIView +from arcade.gui.experimental.password_input import UIPasswordInput +from arcade.gui.widgets.buttons import UIFlatButton +from arcade.gui.widgets.layout import UIGridLayout, UIAnchorLayout +from arcade.gui.widgets.text import UILabel +from arcade import resources + +# Load kenny fonts shipped with arcade +resources.load_system_fonts() + + +class MyView(UIView): + def __init__(self): + super().__init__() + self.background_color = arcade.uicolor.BLUE_BELIZE_HOLE + + grid = UIGridLayout( + size_hint=(0, 0), # wrap children + row_count=5, # title | user, pw | login button + column_count=2, # label and input field + vertical_spacing=10, + horizontal_spacing=5, + ) + grid.with_padding(all=50) + grid.with_background(color=arcade.uicolor.GREEN_GREEN_SEA) + + title = grid.add( + UILabel(text="Login", width=150, font_size=20, font_name="Kenney Future"), + column=0, + row=0, + column_span=2, + ) + title.with_padding(bottom=20) + + grid.add(UILabel(text="Username:", width=80, font_name="Kenney Future"), column=0, row=1) + self.username_input = grid.add( + UIInputText(width=150, font_name="Kenney Future"), column=1, row=1 + ) + + grid.add(UILabel(text="Password:", width=80, font_name="Kenney Future"), column=0, row=2) + self.password_input = grid.add( + UIPasswordInput(width=150, font_name="Kenney Future"), column=1, row=2 + ) + self.password_input.with_background(color=arcade.uicolor.GREEN_GREEN_SEA) + # set background to prevent full render on blinking caret + + self.login_button = grid.add( + UIFlatButton(text="Login", height=30, width=150, size_hint=(1, None)), + column=0, + row=3, + column_span=2, + ) + self.login_button.on_click = self.on_login + + # add warning label + self.warning_label = grid.add( + UILabel( + text="Use [enter] to switch fields, then enter to login", + width=150, + font_size=10, + font_name="Kenney Future", + ), + column=0, + row=4, + column_span=2, + ) + + anchor = UIAnchorLayout() # to center grid on screen + anchor.add(grid) + + self.add_widget(anchor) + + # activate username input field + self.username_input.activate() + + def on_key_press(self, symbol: int, modifiers: int) -> bool | None: + # if username field active, switch fields with enter + if self.username_input.active: + if symbol == arcade.key.ENTER: + self.username_input.deactivate() + self.password_input.activate() + return True + # if password field active, login with enter + elif self.password_input.active: + if symbol == arcade.key.ENTER: + self.password_input.deactivate() + self.on_login(None) + return True + return False + + def on_login(self, event: UIOnClickEvent | None): + username = self.username_input.text.strip() + password = self.password_input.text.strip() + print(f"User logged in with: {username} {password}") + + +if __name__ == "__main__": + window = arcade.Window(title="GUI Example: Hidden Password") + window.show_view(MyView()) + window.run() diff --git a/arcade/gui/examples/exp_scroll_area.py b/arcade/examples/gui/exp_scroll_area.py similarity index 96% rename from arcade/gui/examples/exp_scroll_area.py rename to arcade/examples/gui/exp_scroll_area.py index 8c070e610d..e618fb0a9e 100644 --- a/arcade/gui/examples/exp_scroll_area.py +++ b/arcade/examples/gui/exp_scroll_area.py @@ -11,7 +11,7 @@ * UIScrollBars If arcade and Python are properly installed, you can run this example with: -python -m arcade.gui.examples.exp_scroll_area +python -m arcade.examples.gui.exp_scroll_area """ from __future__ import annotations diff --git a/arcade/gui/examples/grid_layout.py b/arcade/examples/gui/grid_layout.py similarity index 62% rename from arcade/gui/examples/grid_layout.py rename to arcade/examples/gui/grid_layout.py index 276f4d4331..ddb6c33644 100644 --- a/arcade/gui/examples/grid_layout.py +++ b/arcade/examples/gui/grid_layout.py @@ -5,7 +5,7 @@ use the col_span and row_span keyword arguments. If arcade and Python are properly installed, you can run this example with: -python -m arcade.gui.examples.grid_layout +python -m arcade.examples.gui.grid_layout """ from __future__ import annotations @@ -21,12 +21,12 @@ def __init__(self): super().__init__() self.ui = UIManager() - dummy1 = UIDummy(width=100, height=100) + dummy1 = UIDummy(size_hint=(1, 1)) dummy2 = UIDummy(width=50, height=50) dummy3 = UIDummy(width=50, height=50, size_hint=(0.5, 0.5)) - dummy4 = UIDummy(width=100, height=100) - dummy5 = UIDummy(width=200, height=100) - dummy6 = UIDummy(width=100, height=300) + dummy4 = UIDummy(size_hint=(1, 1)) + dummy5 = UIDummy(size_hint=(1, 1)) + dummy6 = UIDummy(size_hint=(1, 1)) subject = ( UIGridLayout( @@ -34,22 +34,26 @@ def __init__(self): row_count=3, size_hint=(0.5, 0.5), ) - .with_border() - .with_padding() + .with_border(color=arcade.color.RED) + .with_padding(all=2) ) - subject.add(child=dummy1, col_num=0, row_num=0) - subject.add(child=dummy2, col_num=0, row_num=1) - subject.add(child=dummy3, col_num=1, row_num=0) - subject.add(child=dummy4, col_num=1, row_num=1) - subject.add(child=dummy5, col_num=0, row_num=2, col_span=2) - subject.add(child=dummy6, col_num=2, row_num=0, row_span=3) + subject.add(child=dummy1, column=0, row=0) + subject.add(child=dummy2, column=0, row=1) + subject.add(child=dummy3, column=1, row=0) + subject.add(child=dummy4, column=1, row=1) + subject.add(child=dummy5, column=0, row=2, column_span=2) + subject.add(child=dummy6, column=2, row=0, row_span=3) anchor = UIAnchorLayout() anchor.add(subject) self.ui.add(anchor) + self.ui.execute_layout() + print(subject.size) + self.grid = subject + def on_show_view(self): self.window.background_color = arcade.color.DARK_BLUE_GRAY # Enable UIManager when view is shown to catch window events @@ -59,6 +63,11 @@ def on_hide_view(self): # Disable UIManager when view gets inactive self.ui.disable() + def on_key_press(self, symbol: int, modifiers: int) -> bool | None: + if symbol == arcade.key.D: + self.grid.legacy_mode = not self.grid.legacy_mode + return True + def on_draw(self): self.clear() self.ui.draw() diff --git a/arcade/gui/examples/ninepatch.py b/arcade/examples/gui/ninepatch.py similarity index 97% rename from arcade/gui/examples/ninepatch.py rename to arcade/examples/gui/ninepatch.py index 22e9623945..17cef462e7 100644 --- a/arcade/gui/examples/ninepatch.py +++ b/arcade/examples/gui/ninepatch.py @@ -6,7 +6,7 @@ same, like a window. If arcade and Python are properly installed, you can run this example with: -python -m arcade.gui.examples.ninepatch +python -m arcade.examples.gui.ninepatch """ from __future__ import annotations diff --git a/arcade/gui/examples/side_bars_with_box_layout.py b/arcade/examples/gui/side_bars_with_box_layout.py similarity index 97% rename from arcade/gui/examples/side_bars_with_box_layout.py rename to arcade/examples/gui/side_bars_with_box_layout.py index 4cdfa24c96..6c995261a3 100644 --- a/arcade/gui/examples/side_bars_with_box_layout.py +++ b/arcade/examples/gui/side_bars_with_box_layout.py @@ -9,7 +9,7 @@ objects which contain other widgets. If arcade and Python are properly installed, you can run this example with: -python -m arcade.gui.examples.side_bars_with_box_layout +python -m arcade.examples.gui.side_bars_with_box_layout """ from __future__ import annotations diff --git a/arcade/examples/gui_flat_button.py b/arcade/examples/gui_flat_button.py deleted file mode 100644 index 0ce6f19af7..0000000000 --- a/arcade/examples/gui_flat_button.py +++ /dev/null @@ -1,87 +0,0 @@ -""" -Example code showing how to create a button, -and the three ways to process button events. - -If Python and Arcade are installed, this example can be run from the command line with: -python -m arcade.examples.gui_flat_button -""" -import arcade -import arcade.gui - -# --- Method 1 for handling click events, -# Create a child class. -import arcade.gui.widgets.buttons -import arcade.gui.widgets.layout - - -class QuitButton(arcade.gui.widgets.buttons.UIFlatButton): - def on_click(self, event: arcade.gui.UIOnClickEvent): - arcade.exit() - - -class MyView(arcade.View): - def __init__(self): - super().__init__() - # --- Required for all code that uses UI element, - # a UIManager to handle the UI. - self.ui = arcade.gui.UIManager() - - # Create a vertical BoxGroup to align buttons - self.v_box = arcade.gui.widgets.layout.UIBoxLayout(space_between=20) - - # Create the buttons - start_button = arcade.gui.widgets.buttons.UIFlatButton( - text="Start Game", width=200 - ) - self.v_box.add(start_button) - - settings_button = arcade.gui.widgets.buttons.UIFlatButton( - text="Settings", width=200 - ) - self.v_box.add(settings_button) - - # Again, method 1. Use a child class to handle events. - quit_button = QuitButton(text="Quit", width=200) - self.v_box.add(quit_button) - - # --- Method 2 for handling click events, - # assign self.on_click_start as callback - start_button.on_click = self.on_click_start - - # --- Method 3 for handling click events, - # use a decorator to handle on_click events - @settings_button.event("on_click") - def on_click_settings(event): - print("Settings:", event) - - # Create a widget to hold the v_box widget, that will center the buttons - ui_anchor_layout = arcade.gui.widgets.layout.UIAnchorLayout() - ui_anchor_layout.add(child=self.v_box, anchor_x="center_x", anchor_y="center_y") - - self.ui.add(ui_anchor_layout) - - def on_show_view(self): - self.window.background_color = arcade.color.DARK_BLUE_GRAY - # Enable UIManager when view is shown to catch window events - self.ui.enable() - - def on_hide_view(self): - # Disable UIManager when view gets inactive - self.ui.disable() - - def on_click_start(self, event): - print("Start:", event) - - def on_draw(self): - self.clear() - self.ui.draw() - - def on_key_press(self, symbol: int, modifiers: int): - if symbol == arcade.key.ESCAPE: - self.window.close() - - -if __name__ == '__main__': - window = arcade.Window(1280, 720, "UIExample", resizable=True) - window.show_view(MyView()) - window.run() diff --git a/arcade/examples/gui_flat_button_styled.py b/arcade/examples/gui_flat_button_styled.py deleted file mode 100644 index 713d2a1be3..0000000000 --- a/arcade/examples/gui_flat_button_styled.py +++ /dev/null @@ -1,98 +0,0 @@ -""" -Example code showing how to style UIFlatButtons. - -If Python and Arcade are installed, this example can be run from the command line with: -python -m arcade.examples.gui_flat_button_styled -""" -import arcade -import arcade.gui -import arcade.gui.widgets.buttons -import arcade.gui.widgets.layout -from arcade.gui import UIFlatButton - - -class MyView(arcade.View): - def __init__(self): - super().__init__() - # --- Required for all code that uses UI element, - # a UIManager to handle the UI. - self.ui = arcade.gui.UIManager() - - # Render button - red_style = { - "normal": UIFlatButton.UIStyle( - font_size=12, - font_name=("calibri", "arial"), - font_color=arcade.color.WHITE, - bg=arcade.color.RED, - border=None, - border_width=0, - ), - "hover": UIFlatButton.UIStyle( - font_size=12, - font_name=("calibri", "arial"), - font_color=arcade.color.WHITE, - bg=arcade.color.REDWOOD, - border=arcade.color.RED, - border_width=2, - ), - "press": UIFlatButton.UIStyle( - font_size=12, - font_name=("calibri", "arial"), - font_color=arcade.color.WHITE, - bg=arcade.color.RED_BROWN, - border=arcade.color.REDWOOD, - border_width=2, - ), - "disabled": UIFlatButton.UIStyle( - font_size=12, - font_name=("calibri", "arial"), - font_color=arcade.color.WHITE, - bg=arcade.color.COOL_GREY, - border=arcade.color.ASH_GREY, - border_width=2, - ) - } - - # Create a vertical BoxGroup to align buttons - self.v_box = arcade.gui.widgets.layout.UIBoxLayout(space_between=20) - - # Create the buttons - demo_button_1 = arcade.gui.widgets.buttons.UIFlatButton( - text="Demo 1", width=200, style=UIFlatButton.DEFAULT_STYLE - ) - demo_button_2 = arcade.gui.widgets.buttons.UIFlatButton( - text="Demo 2", width=200, style=red_style - ) - - self.v_box.add(demo_button_1) - self.v_box.add(demo_button_2) - - # Create a widget to hold the v_box widget, that will center the buttons - ui_anchor_layout = arcade.gui.widgets.layout.UIAnchorLayout() - ui_anchor_layout.add(child=self.v_box, anchor_x="center_x", anchor_y="center_y") - - self.ui.add(ui_anchor_layout) - - def on_show_view(self): - self.window.background_color = arcade.color.DARK_BLUE_GRAY - # Enable UIManager when view is shown to catch window events - self.ui.enable() - - def on_hide_view(self): - # Disable UIManager when view gets inactive - self.ui.disable() - - def on_draw(self): - self.clear() - self.ui.draw() - - def on_key_press(self, symbol: int, modifiers: int): - if symbol == arcade.key.ESCAPE: - self.window.close() - - -if __name__ == '__main__': - window = arcade.Window(1280, 720, "UIExample", resizable=True) - window.show_view(MyView()) - window.run() diff --git a/arcade/examples/gui_ok_messagebox.py b/arcade/examples/gui_ok_messagebox.py deleted file mode 100644 index 678dd9d2f3..0000000000 --- a/arcade/examples/gui_ok_messagebox.py +++ /dev/null @@ -1,94 +0,0 @@ -""" -Example code showing how to use the OKMessageBox - -If Python and Arcade are installed, this example can be run from the command line with: -python -m arcade.examples.gui_ok_messagebox -""" -import arcade -import arcade.gui -import arcade.gui.widgets.buttons -import arcade.gui.widgets.layout -from arcade.gui import UIOnClickEvent -from arcade.gui.events import UIOnActionEvent - - -class MyView(arcade.View): - def __init__(self): - super().__init__() - # Create and enable the UIManager - self.ui = arcade.gui.UIManager() - - # Create a box group to align the 'open' button in the center - self.v_box = arcade.gui.widgets.layout.UIBoxLayout() - - # Create a button. We'll click on this to open our window. - show_message_box_button = arcade.gui.widgets.buttons.UIFlatButton( - text="Show Message Box", width=300 - ) - # Create a label to show the user's choices - self.last_choice = arcade.gui.UILabel( - text="", - align="left", width=300 - ) - - # Add both widgets to the v_box to center them - self.v_box.add(show_message_box_button) - self.v_box.add(self.last_choice) - - # Add a hook to run when we click on the button. - show_message_box_button.on_click = self.on_click_open - self.open_message_box_button = show_message_box_button - - # Create a widget to hold the v_box widget, that will center the buttons - ui_anchor_layout = arcade.gui.widgets.layout.UIAnchorLayout() - ui_anchor_layout.add(child=self.v_box, anchor_x="center_x", anchor_y="center_y") - self.ui.add(ui_anchor_layout) - - def on_click_open(self, _: UIOnClickEvent): - # The code in this function is run when we click the ok button. - # The code below opens the message box and auto-dismisses it when done. - message_box = arcade.gui.UIMessageBox( - width=300, - height=200, - message_text=( - "Which option do you choose?" - ), - buttons=["Ok", "Cancel"], - ) - - @message_box.event("on_action") - def on_message_box_close(e: UIOnActionEvent): - # Update the last_choice display - self.last_choice.text = f"User pressed {e.action}." - self.last_choice.fit_content() # Important! Update the layout! - - # show open button and allow interaction again - self.open_message_box_button.visible = True - - # hide open button and prevent interaction - self.open_message_box_button.visible = False - - self.ui.add(message_box) - - def on_show_view(self): - self.window.background_color = arcade.color.DARK_BLUE_GRAY - # Enable UIManager when view is shown to catch window events - self.ui.enable() - - def on_hide_view(self): - # Disable UIManager when view gets inactive - self.ui.disable() - - def on_draw(self): - self.clear() - self.ui.draw() - - def on_key_press(self, symbol: int, modifiers: int): - if symbol == arcade.key.ESCAPE: - self.window.close() - - -if __name__ == '__main__': - window = arcade.Window(1280, 720, "UIExample", resizable=True) - window.show_view(MyView()) - window.run() diff --git a/arcade/examples/gui_scrollable_text.py b/arcade/examples/gui_scrollable_text.py deleted file mode 100644 index 4f5fcac781..0000000000 --- a/arcade/examples/gui_scrollable_text.py +++ /dev/null @@ -1,98 +0,0 @@ -""" -If Python and Arcade are installed, this example can be run from the command line with: -python -m arcade.examples.gui_scrollable_text -""" -import arcade -from arcade import load_texture - -from arcade.gui import UIManager, UIInputText, UITextArea -from arcade.gui.nine_patch import NinePatchTexture - -# This is a special "nine-patch" texture that will stretch in the middle, but -# always keep the corners the same size. This is useful for making panels that -# can be any size. -TEX_GREY_PANEL = load_texture(":resources:gui_basic_assets/window/grey_panel.png") - -LOREM_IPSUM = ( - "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent eget " - "pellentesque velit. Nam eu rhoncus nulla. Fusce ornare libero eget ex " - "vulputate, vitae mattis orci eleifend. Donec quis volutpat arcu. Proin " - "lacinia velit id imperdiet ultrices. Fusce porta magna leo, " - "non maximus justo facilisis vel. Duis pretium sem ut eros scelerisque, " - "a dignissim ante pellentesque. Cras rutrum aliquam fermentum. Donec id " - " mollis mi.\n" - "\n" - "Nullam vitae nunc aliquet, lobortis purus eget, porttitor purus. Curabitur " - "feugiat purus sit amet finibus accumsan. Proin varius, enim in pretium " - "pulvinar, augue erat pellentesque ipsum, sit amet varius leo risus quis " - "tellus. Donec posuere ligula risus, et scelerisque nibh cursus ac. " - "Mauris feugiat tortor turpis, vitae imperdiet mi euismod aliquam. Fusce " - "vel ligula volutpat, finibus sapien in, lacinia lorem. Proin tincidunt " - "gravida nisl in pellentesque. Aenean sed arcu ipsum. Vivamus quam arcu, " - "elementum nec auctor non, convallis non elit. Maecenas id scelerisque " - "lectus. Vivamus eget sem tristique, dictum lorem eget, maximus leo. " - "Mauris lorem tellus, molestie eu orci ut, porta aliquam est. Nullam " - "lobortis tempor magna, egestas lacinia lectus.\n" -) - - -class MyView(arcade.View): - def __init__(self): - super().__init__() - self.ui = UIManager() - - # The specific configuration for grey_panel.png - # 5 x 5 pixel corners that won't be stretched. - bg_tex = NinePatchTexture( - left=5, - right=5, - top=5, - bottom=5, - texture=TEX_GREY_PANEL, - ) - - text_area = UITextArea( - x=100, - y=400, - width=300, - height=200, - text=LOREM_IPSUM, - text_color=(0, 0, 0, 255), - ) - - self.ui.add(text_area.with_padding(all=15).with_background(texture=bg_tex)) - - self.ui.add( - UIInputText(x=340, y=200, width=200, height=50, text="Hello") - .with_background(texture=bg_tex) - .with_padding(all=10) - ) - - self.ui.add( - UIInputText( - x=340, y=110, width=200, height=50, text="", multiline=True, - ).with_border(), - ) - - def on_show_view(self): - self.window.background_color = arcade.color.DARK_BLUE_GRAY - # Enable UIManager when view is shown to catch window events - self.ui.enable() - - def on_hide_view(self): - # Disable UIManager when view gets inactive - self.ui.disable() - - def on_draw(self): - self.clear() - self.ui.draw() - - def on_key_press(self, symbol: int, modifiers: int): - if symbol == arcade.key.ESCAPE: - self.window.close() - - -if __name__ == '__main__': - window = arcade.Window(1280, 720, "UIExample", resizable=True) - window.show_view(MyView()) - window.run() diff --git a/arcade/examples/gui_slider.py b/arcade/examples/gui_slider.py deleted file mode 100644 index 3bed6a9f30..0000000000 --- a/arcade/examples/gui_slider.py +++ /dev/null @@ -1,68 +0,0 @@ -""" -GUI Slider Example - -If Python and Arcade are installed, this example can be run from the -command line with: -python -m arcade.examples.view_screens_minimal - -This example demonstrates how to create a GUI slider and react to -changes in its value. - -There are two other ways of handling update events. For more -information on this subject, see the gui_flat_button example. - -If Python and Arcade are installed, this example can be run from the command line with: -python -m arcade.examples.gui_slider -""" -import arcade -from arcade.gui import UIManager, UILabel -from arcade.gui.events import UIOnChangeEvent -from arcade.gui.widgets.slider import UISlider - - -class MyView(arcade.View): - def __init__(self): - super().__init__() - # Required, create a UI manager to handle all UI widgets - self.ui = UIManager() - - # Create our pair of widgets - ui_slider = UISlider(value=50, width=600, height=50) - label = UILabel(text=f"{ui_slider.value:02.0f}", font_size=20) - - # Change the label's text whenever the slider is dragged - # See the gui_flat_button example for more information. - @ui_slider.event() - def on_change(event: UIOnChangeEvent): - label.text = f"{ui_slider.value:02.0f}" - label.fit_content() - - # Create a layout to hold the label and the slider - ui_anchor_layout = arcade.gui.widgets.layout.UIAnchorLayout() - ui_anchor_layout.add( - child=ui_slider, - anchor_x="center_x", - anchor_y="center_y" - ) - ui_anchor_layout.add(child=label, align_y=50) - - self.ui.add(ui_anchor_layout) - - def on_show_view(self): - self.window.background_color = arcade.color.DARK_BLUE_GRAY - # Enable UIManager when view is shown to catch window events - self.ui.enable() - - def on_hide_view(self): - # Disable UIManager when view gets inactive - self.ui.disable() - - def on_draw(self): - self.clear() - self.ui.draw() - - -if __name__ == '__main__': - window = arcade.Window(1280, 720, "UIExample", resizable=True) - window.show_view(MyView()) - window.run() diff --git a/arcade/examples/gui_widgets.py b/arcade/examples/gui_widgets.py deleted file mode 100644 index b064bd7376..0000000000 --- a/arcade/examples/gui_widgets.py +++ /dev/null @@ -1,94 +0,0 @@ -""" -Example code showing how to create some of the different UIWidgets. - -If Python and Arcade are installed, this example can be run from the command line with: -python -m arcade.examples.gui_widgets -""" -import arcade -import arcade.gui -import arcade.gui.widgets.buttons -import arcade.gui.widgets.layout -import arcade.gui.widgets.text - -# Load fonts bundled with arcade such as the Kenney fonts -arcade.resources.load_system_fonts() - - -class MyView(arcade.View): - def __init__(self): - super().__init__() - # --- Required for all code that uses UI element, - # a UIManager to handle the UI. - self.ui = arcade.gui.UIManager() - - # Create a vertical BoxGroup to align buttons - self.v_box = arcade.gui.widgets.layout.UIBoxLayout(space_between=20) - - # Create a text label - ui_text_label = arcade.gui.widgets.text.UITextArea( - text="This is a Text Widget", - width=600, - height=40, - font_size=24, - font_name="Kenney Future", - ) - self.v_box.add(ui_text_label) - - text = ( - "The real danger is not that computers will begin to think like people, " - "but that people will begin " - "to think like computers. - Sydney Harris (Journalist)" - ) - ui_text_label = arcade.gui.widgets.text.UITextArea( - text=text, width=450, height=60, font_size=12, font_name="Arial" - ) - self.v_box.add(ui_text_label) - - # Create a UIFlatButton - ui_flatbutton = arcade.gui.widgets.buttons.UIFlatButton( - text="Flat Button", width=200 - ) - self.v_box.add(ui_flatbutton) - - # Handle Clicks - @ui_flatbutton.event("on_click") - def on_click_flatbutton(event): - print("UIFlatButton pressed", event) - - # Create a UITextureButton - texture = arcade.load_texture(":resources:onscreen_controls/flat_dark/play.png") - ui_texture_button = arcade.gui.widgets.buttons.UITextureButton(texture=texture) - - # Handle Clicks - @ui_texture_button.event("on_click") - def on_click_texture_button(event): - print("UITextureButton pressed", event) - - self.v_box.add(ui_texture_button) - - # Create a widget to hold the v_box widget, that will center the buttons - self.ui.add( - arcade.gui.widgets.layout.UIAnchorLayout(children=[self.v_box]) - ) - - def on_click_start(self, event): - print("Start:", event) - - def on_show_view(self): - self.window.background_color = arcade.color.DARK_BLUE_GRAY - # Enable UIManager when view is shown to catch window events - self.ui.enable() - - def on_hide_view(self): - # Disable UIManager when view gets inactive - self.ui.disable() - - def on_draw(self): - self.clear() - self.ui.draw() - - -if __name__ == '__main__': - window = arcade.Window(1280, 720, "UIExample", resizable=True) - window.show_view(MyView()) - window.run() 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/future/input/__init__.py b/arcade/future/input/__init__.py index cba31b585d..73d7b86eb1 100644 --- a/arcade/future/input/__init__.py +++ b/arcade/future/input/__init__.py @@ -4,3 +4,17 @@ from .inputs import ControllerAxes, ControllerButtons, Keys, MouseAxes, MouseButtons from .manager import ActionState, InputManager from .input_mapping import Action, ActionMapping, Axis, AxisMapping + +__all__ = [ + "ControllerAxes", + "ControllerButtons", + "Keys", + "MouseAxes", + "MouseButtons", + "ActionState", + "InputManager", + "Action", + "ActionMapping", + "Axis", + "AxisMapping", +] diff --git a/arcade/future/sub_clock.py b/arcade/future/sub_clock.py new file mode 100644 index 0000000000..cc73d04837 --- /dev/null +++ b/arcade/future/sub_clock.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +from typing import Optional, Union + +from arcade.clock import GLOBAL_CLOCK, Clock + + +def boot_strap_clock(clock: Optional[Clock] = None) -> Clock: + """ + Because the sub_clock is not a fully featured part of arcade we have to + manipulate the clocks before the can be used with sub_clocks. + + This step will no longer be required when SubClocks become part of the main + library. + + calling it will boostrap the global clock. + DO NOT CALL MORE THAN ONCE PER CLOCK. + + Args: + clock: a clcok that has yet to be setup. Defaults to arcade.clock.GLOBAL_CLOCK. + """ + clock = clock or GLOBAL_CLOCK + + if hasattr(clock, "children"): + raise ValueError(f"The clock {clock} has already been bootstrapped.") + + # No type check will ever like this, but we do what we must. + clock.children = [] # type: ignore + + def recursive_tick(delta_time: float) -> None: + clock.tick(delta_time) + for child in clock.children: # type: ignore + child.tick(clock._tick_delta_time) + + # Hey we did a decorator manually! what a time to be alive. + clock.tick = recursive_tick # type: ignore + + def add_child(child: SubClock) -> None: + clock.children.append(child) # type: ignore + + clock.add_child = add_child # type: ignore + + return clock + + +class SubClock(Clock): + """ + A SubClock which gets ticked by a parent clock and can have its flow + of time altered independantly of its parent or siblings. + + Args: + parent: The clock which will tick the SubClock. + could be the GLOBAL_CLOCK or another SubClock + tick_speed: A multiplier on how the 'speed' of time. + i.e. a value of 0.5 means time elapsed half as fast for this clock. Defaults to 1.0. + """ + + def __init__(self, parent: Union[Clock, SubClock, None] = None, tick_speed: float = 1) -> None: + parent = parent or GLOBAL_CLOCK + super().__init__(parent._elapsed_time, parent._tick, tick_speed) + self.children: list[SubClock] = [] + try: + parent.add_child(self) # type: ignore + except AttributeError: + raise AttributeError( + f"The clock {parent} has not been bootstrapped properly" + f"call boot_strap_clock({parent}) before adding children" + ) + + def add_child(self, child: SubClock) -> None: + self.children.append(child) + + def tick(self, delta_time: float) -> None: + super().tick(delta_time) + + for child in self.children: + child.tick(self.delta_time) diff --git a/arcade/gui/constructs.py b/arcade/gui/constructs.py index 256bb99ad6..f5470c105f 100644 --- a/arcade/gui/constructs.py +++ b/arcade/gui/constructs.py @@ -5,12 +5,13 @@ from typing import Any, Optional import arcade +from arcade import uicolor from arcade.gui.events import UIOnActionEvent, UIOnClickEvent from arcade.gui.mixins import UIMouseFilterMixin from arcade.gui.nine_patch import NinePatchTexture from arcade.gui.widgets.buttons import UIFlatButton from arcade.gui.widgets.layout import UIAnchorLayout, UIBoxLayout -from arcade.gui.widgets.text import UITextArea +from arcade.gui.widgets.text import UILabel, UITextArea class UIMessageBox(UIMouseFilterMixin, UIAnchorLayout): @@ -38,19 +39,20 @@ def __init__( width: float, height: float, message_text: str, + title: str | None = None, buttons=("Ok",), ): if not buttons: raise ValueError("At least a single value has to be available for `buttons`") super().__init__(size_hint=(1, 1)) - self.register_event_type("on_action") # type: ignore # https://github.com/pyglet/pyglet/pull/1173 # noqa + self.register_event_type("on_action") + self.with_background(color=uicolor.GRAY_CONCRETE.replace(a=150)) space = 20 # setup frame which will act like the window frame = self.add(UIAnchorLayout(width=width, height=height, size_hint=None)) - frame.with_padding(all=space) frame.with_background( texture=NinePatchTexture( @@ -58,12 +60,29 @@ def __init__( right=7, bottom=7, top=7, - texture=arcade.load_texture(":resources:gui_basic_assets/window/grey_panel.png"), + texture=arcade.load_texture(":resources:gui_basic_assets/window/panel_gray.png"), ) ) + # setup title + if title: + title_label = frame.add( + child=UILabel( + text="Message", + font_size=16, + size_hint=(1, 0), + align="center", + ), + anchor_y="top", + ) + title_label.with_padding(all=2, bottom=5) + title_label.with_background(color=uicolor.DARK_BLUE_MIDNIGHT_BLUE) + title_offset = title_label.height + else: + title_offset = 0 + # Setup text - frame.add( + text_area = frame.add( child=UITextArea( text=message_text, width=width - space, @@ -72,7 +91,9 @@ def __init__( ), anchor_x="center", anchor_y="top", + align_y=-(title_offset + space), ) + text_area.with_padding(all=10) # setup buttons button_group = UIBoxLayout(vertical=False, space_between=10) @@ -82,9 +103,7 @@ def __init__( button.on_click = self._on_choice # type: ignore frame.add( - child=button_group, - anchor_x="right", - anchor_y="bottom", + child=button_group, anchor_x="right", anchor_y="bottom", align_x=-space, align_y=space ) def _on_choice(self, event): @@ -135,26 +154,21 @@ def __init__( space_between=space_between, **kwargs, ) - self.register_event_type("on_action") # type: ignore # https://github.com/pyglet/pyglet/pull/1173 # noqa + self.register_event_type("on_action") self.button_factory = button_factory - def add_button( - self, - label: str, - *, - style=None, - multiline=False, - ): + def add_button(self, label: str, *, style=None, multiline=False, **kwargs): """Add a button to the row. Args: label: The text of the button. style: The style of the button. multiline: Whether the button is multiline or not. + **kwargs: Passed to the button factory. """ - button = self.button_factory(text=label, style=style, multiline=multiline) - button.on_click = self._on_click # type: ignore + button = self.button_factory(text=label, style=style, multiline=multiline, **kwargs) + button.on_click = self._on_click self.add(button) return button diff --git a/arcade/gui/examples/__init__.py b/arcade/gui/examples/__init__.py deleted file mode 100644 index a451f32b52..0000000000 --- a/arcade/gui/examples/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -"""This package contains GUI specific examples. - -These show explicit GUI examples, and are not part of the main examples. - -Examples which begin with "exp_" use experimental components from the -`arcade.gui.experimental` module. -""" diff --git a/arcade/gui/examples/anchor_layout.py b/arcade/gui/examples/anchor_layout.py deleted file mode 100644 index 6f069f3d8a..0000000000 --- a/arcade/gui/examples/anchor_layout.py +++ /dev/null @@ -1,82 +0,0 @@ -"""Positioning UI elements with UIAnchorLayout - -UIAnchorLayout aligns widgets added to it to directional anchors, which -include "left", "center_x", or "top". Dummy widgets react to click events -by changing color. - -If arcade and Python are properly installed, you can run this example with: -python -m arcade.gui.examples.anchor_layout -""" - -from __future__ import annotations - -import arcade -from arcade.gui import UIManager -from arcade.gui.widgets import UIDummy -from arcade.gui.widgets.layout import UIAnchorLayout - - -class MyView(arcade.View): - def __init__(self): - super().__init__() - self.ui = UIManager() - - anchor = self.ui.add(UIAnchorLayout()) - - anchor.add( - child=UIDummy(), - anchor_x="center_x", - anchor_y="top", - ) - - anchor.add( - child=UIDummy(), - anchor_x="right", - anchor_y="center_y", - ) - - anchor.add( - child=UIDummy(), - anchor_x="center_x", - anchor_y="center_y", - ) - - anchor.add( - child=UIDummy(), - anchor_x="left", - anchor_y="bottom", - ) - - anchor.add( - child=UIDummy(), - anchor_x="left", - align_x=20, - anchor_y="center_y", - ) - - anchor.add( - child=UIDummy(), - anchor_x="right", - align_x=-40, - anchor_y="bottom", - align_y=40, - ) - - def on_show_view(self): - self.window.background_color = arcade.color.DARK_BLUE_GRAY - # Enable UIManager when view is shown to catch window events - self.ui.enable() - - def on_hide_view(self): - # Disable UIManager when view gets inactive - self.ui.disable() - - def on_draw(self): - self.clear() - self.ui.draw() - - -if __name__ == "__main__": - window = arcade.Window(800, 600, "UIExample", resizable=True) - window.show_view(MyView()) - window.run() diff --git a/arcade/gui/examples/box_layout.py b/arcade/gui/examples/box_layout.py deleted file mode 100644 index 68f98b0d20..0000000000 --- a/arcade/gui/examples/box_layout.py +++ /dev/null @@ -1,74 +0,0 @@ -"""Arrange widgets in vertical or horizontal lines with UIBoxLayout - -The direction UIBoxLayout follows is controlled by the `vertical` keyword -argument. It is True by default. Pass False to it to arrange elements in -a horizontal line. - -If arcade and Python are properly installed, you can run this example with: -python -m arcade.gui.examples.box_layout -""" - -from __future__ import annotations - -import arcade -from arcade.gui import UIManager, UIBoxLayout -from arcade.gui.widgets import UIDummy, UISpace -from arcade.gui.widgets.layout import UIAnchorLayout - - -class MyView(arcade.View): - def __init__(self): - super().__init__() - self.ui = UIManager() - - anchor = self.ui.add(UIAnchorLayout()) - - self.v_box = UIBoxLayout( - children=[ - UIDummy(width=200, color=arcade.color.RED), - UIDummy(width=200, color=arcade.color.YELLOW), - UIDummy(width=200, color=arcade.color.GREEN), - ], - space_between=20, - ).with_border() - anchor.add( - align_x=200, - anchor_x="center_x", - anchor_y="center_y", - child=self.v_box, - ) - - self.h_box = UIBoxLayout( - vertical=False, - children=[ - UIDummy(width=100, color=arcade.color.RED), - UISpace(width=20, height=100), - UIDummy(width=50, color=arcade.color.YELLOW).with_padding(right=30), - UIDummy(width=100, color=arcade.color.GREEN), - ], - ) - anchor.add( - child=self.h_box.with_border(), - anchor_x="center_x", - anchor_y="center_y", - align_x=-200, - ) - - def on_show_view(self): - self.window.background_color = arcade.color.DARK_BLUE_GRAY - # Enable UIManager when view is shown to catch window events - self.ui.enable() - - def on_hide_view(self): - # Disable UIManager when view gets inactive - self.ui.disable() - - def on_draw(self): - self.clear() - self.ui.draw() - - -if __name__ == "__main__": - window = arcade.Window(800, 600, "UIExample", resizable=True) - window.show_view(MyView()) - window.run() diff --git a/arcade/gui/examples/exp_hidden_password.py b/arcade/gui/examples/exp_hidden_password.py deleted file mode 100644 index 129bfd02db..0000000000 --- a/arcade/gui/examples/exp_hidden_password.py +++ /dev/null @@ -1,71 +0,0 @@ -"""Creating a hidden password field - -This example demonstrates how to create a custom text input -which hides the contents behind a custom character, as often -required for login screens - -If arcade and Python are properly installed, you can run this example with: -python -m arcade.gui.examples.exp_hidden_password -""" - -from __future__ import annotations - -import arcade -from arcade.gui import UIManager, UIInputText, UIOnClickEvent -from arcade.gui.experimental.password_input import UIPasswordInput -from arcade.gui.widgets.buttons import UIFlatButton -from arcade.gui.widgets.layout import UIGridLayout, UIAnchorLayout -from arcade.gui.widgets.text import UILabel - - -# FIXME -class MyView(arcade.View): - def __init__(self): - super().__init__() - self.ui = UIManager() - - grid = UIGridLayout( - size_hint=(0, 0), # wrap children - row_count=3, # user, pw and login button - column_count=2, # label and input field - vertical_spacing=10, - horizontal_spacing=5, - ) - - grid.add(UILabel(text="Username:"), col_num=0, row_num=0) - self.username_input = grid.add(UIInputText(height=25), col_num=1, row_num=0).with_border() - - grid.add(UILabel(text="Password:"), col_num=0, row_num=1) - self.password_input = grid.add( - UIPasswordInput(height=25), col_num=1, row_num=1 - ).with_border() - - self.login_button = grid.add(UIFlatButton(text="Login"), col_num=0, row_num=2, col_span=2) - self.login_button.on_click = self.on_login - - anchor = UIAnchorLayout() # to center grid on screen - anchor.add(grid) - - self.ui.add(anchor) - - def on_login(self, event: UIOnClickEvent): - print(f"User logged in with: {self.username_input.text} {self.password_input.text}") - - def on_show_view(self): - self.window.background_color = arcade.color.DARK_BLUE_GRAY - # Enable UIManager when view is shown to catch window events - self.ui.enable() - - def on_hide_view(self): - # Disable UIManager when view gets inactive - self.ui.disable() - - def on_draw(self): - self.clear() - self.ui.draw() - - -if __name__ == "__main__": - window = arcade.Window(800, 600, "UIExample", resizable=True) - window.show_view(MyView()) - window.run() diff --git a/arcade/gui/examples/stats_topleft.py b/arcade/gui/examples/stats_topleft.py deleted file mode 100644 index ea6fd5e70f..0000000000 --- a/arcade/gui/examples/stats_topleft.py +++ /dev/null @@ -1,93 +0,0 @@ -"""Displaying stats in the window's top left corner - -This example displays numerical stats with labels by using the following: - -* A UILabel subclass which uses string formatting to convert numbers -* Vertical UIBoxLayouts to hold the quantity labels & number columns - -If arcade and Python are properly installed, you can run this example with: -python -m arcade.gui.examples.stats_topleft -""" - -from __future__ import annotations - -import arcade -from arcade.gui import UIManager, UILabel, UIBoxLayout -from arcade.gui.widgets.layout import UIAnchorLayout - - -class UINumberLabel(UILabel): - _value: float = 0 - - def __init__(self, value=0, format="{value:.0f}", *args, **kwargs): - super().__init__(*args, **kwargs) - self.format = format - self.value = value - - @property - def value(self): - return self._value - - @value.setter - def value(self, value): - self._value = value - self.text = self.format.format(value=value) - self.fit_content() - - -class MyView(arcade.View): - def __init__(self): - super().__init__() - # Init UIManager - self.ui = UIManager() - - # Create value labels - self.timer = UINumberLabel(value=20, align="right", size_hint_min=(30, 20)) - wood = UINumberLabel(10, align="right", size_hint_min=(30, 20)) - stone = UINumberLabel(20, align="right", size_hint_min=(30, 20)) - food = UINumberLabel(30, align="right", size_hint_min=(30, 20)) - - # Create a vertical BoxGroup to align labels - self.columns = UIBoxLayout( - vertical=False, - children=[ - # Create one vertical UIBoxLayout per column and add the labels - UIBoxLayout( - vertical=True, - children=[ - UILabel(text="Time:", align="left", width=50), - UILabel(text="Wood:", align="left", width=50), - UILabel(text="Stone:", align="left", width=50), - UILabel(text="Food:", align="left", width=50), - ], - ), - # Create one vertical UIBoxLayout per column and add the labels - UIBoxLayout(vertical=True, children=[self.timer, wood, stone, food]), - ], - ) - - # Use a UIAnchorWidget to place the UILabels in the top left corner - anchor = self.ui.add(UIAnchorLayout()) - anchor.add(align_x=10, anchor_x="left", align_y=-10, anchor_y="top", child=self.columns) - - def on_update(self, delta_time: float): - self.timer.value += delta_time - - def on_show_view(self): - self.window.background_color = arcade.color.DARK_BLUE_GRAY - # Enable UIManager when view is shown to catch window events - self.ui.enable() - - def on_hide_view(self): - # Disable UIManager when view gets inactive - self.ui.disable() - - def on_draw(self): - self.clear() - self.ui.draw() - - -if __name__ == "__main__": - window = arcade.Window(800, 600, "UIExample", resizable=True) - window.show_view(MyView()) - window.run() diff --git a/arcade/gui/examples/style_v2.py b/arcade/gui/examples/style_v2.py deleted file mode 100644 index 6f4aab11f2..0000000000 --- a/arcade/gui/examples/style_v2.py +++ /dev/null @@ -1,138 +0,0 @@ -"""Changing UI styles in response to events - -This example has a button which cycles its appearance through a -repeating list of different styles when pressed, except when it -is disabled by the user. - -If arcade and Python are properly installed, you can run this example with: -python -m arcade.gui.examples.style_v2 -""" - -from __future__ import annotations - -from itertools import cycle - -import arcade -from arcade.gui import UIManager, UIOnClickEvent -from arcade.gui.constructs import UIButtonRow -from arcade.gui.widgets.buttons import UIFlatButton -from arcade.gui.widgets.layout import UIAnchorLayout - -STYLES = [ - UIFlatButton.DEFAULT_STYLE, - { # RED - "normal": UIFlatButton.UIStyle( - font_size=12, - font_name=("calibri", "arial"), - font_color=arcade.color.WHITE, - bg=arcade.color.RED, - border=None, - border_width=0, - ), - "hover": UIFlatButton.UIStyle( - font_size=12, - font_name=("calibri", "arial"), - font_color=arcade.color.WHITE, - bg=arcade.color.RED, - border=arcade.color.RED_ORANGE, - border_width=2, - ), - "press": UIFlatButton.UIStyle( - font_size=12, - font_name=("calibri", "arial"), - font_color=arcade.color.BLACK, - bg=arcade.color.RED_ORANGE, - border=arcade.color.RED_DEVIL, - border_width=2, - ), - "disabled": UIFlatButton.UIStyle( - font_size=12, - font_name=("calibri", "arial"), - font_color=arcade.color.GRAY, - bg=arcade.color.DARK_SLATE_GRAY, - border=arcade.color.DAVY_GREY, - border_width=2, - ), - }, - { # BLUE - "normal": UIFlatButton.UIStyle( - font_size=12, - font_name=("calibri", "arial"), - font_color=arcade.color.WHITE, - bg=arcade.color.BLUE, - border=None, - border_width=0, - ), - "hover": UIFlatButton.UIStyle( - font_size=12, - font_name=("calibri", "arial"), - font_color=arcade.color.WHITE, - bg=arcade.color.BLUE, - border=arcade.color.BLUE_BELL, - border_width=2, - ), - "press": UIFlatButton.UIStyle( - font_size=12, - font_name=("calibri", "arial"), - font_color=arcade.color.BLACK, - bg=arcade.color.BLUE_BELL, - border=arcade.color.BLUE_GRAY, - border_width=2, - ), - "disabled": UIFlatButton.UIStyle( - font_size=12, - font_name=("calibri", "arial"), - font_color=arcade.color.GRAY, - bg=arcade.color.GRAY_BLUE, - border=arcade.color.DAVY_GREY, - border_width=2, - ), - }, -] - - -class MyView(arcade.View): - def __init__(self): - super().__init__() - # Init UIManager - self.ui = UIManager() - - # Use a UIAnchorWidget to place the UILabels in the top left corner - anchor = self.ui.add(UIAnchorLayout()) - row = anchor.add(UIButtonRow(button_factory=UIFlatButton)) - - button1 = row.add_button("Click me to switch style", multiline=True) - - style_options = cycle(STYLES) - next(style_options) # skip default style in first round - - @button1.event("on_click") - def change_style(event: UIOnClickEvent): - btn: UIFlatButton = event.source - btn.style = next(style_options) - btn.trigger_render() - - button2 = row.add_button("Toggle disable", multiline=True) - - @button2.event("on_click") - def toggle(*_): - button1.disabled = not button1.disabled - - def on_show_view(self): - self.window.background_color = arcade.color.DARK_BLUE_GRAY - # Enable UIManager when view is shown to catch window events - self.ui.enable() - - def on_hide_view(self): - # Disable UIManager when view gets inactive - self.ui.disable() - - def on_draw(self): - self.clear() - self.ui.draw() - - -if __name__ == "__main__": - window = arcade.Window(800, 600, "UIExample", resizable=True) - window.show_view(MyView()) - window.run() diff --git a/arcade/gui/examples/textured_slider.py b/arcade/gui/examples/textured_slider.py deleted file mode 100644 index dcb33cf137..0000000000 --- a/arcade/gui/examples/textured_slider.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Create a slider using textures. - -The initial theme is a 90s sci-fi style, but you can replace the textures -in this example to match the theme of your project. - -If arcade and Python are properly installed, you can run this example with: -python -m arcade.gui.examples.textured_slider -""" - -import arcade -from arcade.gui import UIManager, UIAnchorLayout -from arcade.gui.widgets.slider import UITextureSlider - - -class MyView(arcade.View): - def __init__(self): - super().__init__() - self.ui = UIManager() - - track_tex = arcade.load_texture(":resources:gui_basic_assets/slider_track.png") - thumb_tex = arcade.load_texture(":resources:gui_basic_assets/slider_thumb.png") - self.slider = UITextureSlider(track_tex, thumb_tex) - - @self.slider.event - def on_change(event): - print(f"Slider value: {event}") - - # Add button to UIManager, use UIAnchorWidget defaults to center on screen - self.ui.add(UIAnchorLayout(children=[self.slider])) - - def on_show_view(self): - self.window.background_color = arcade.color.DARK_BLUE_GRAY - # Enable UIManager when view is shown to catch window events - self.ui.enable() - - def on_hide_view(self): - self.ui.disable() - - def on_key_press(self, key, modifiers): - if key == arcade.key.SPACE: - self.slider.disabled = not self.slider.disabled - - def on_draw(self): - self.clear() - self.ui.draw() - - -if __name__ == "__main__": - window = arcade.Window(800, 600, "UIExample", resizable=True) - window.show_view(MyView()) - window.run() diff --git a/arcade/gui/examples/toggle.py b/arcade/gui/examples/toggle.py deleted file mode 100644 index c333aa5be8..0000000000 --- a/arcade/gui/examples/toggle.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Use a custom texture for a toggle button. - -The current theme is a 90s sci-fi style, but you can replace the textures -to match the theme of your game. - -If arcade and Python are properly installed, you can run this example with: -python -m arcade.gui.examples.toggle -""" - -from __future__ import annotations - -import arcade -from arcade import View, load_texture -from arcade.gui import UIManager, UIAnchorLayout -from arcade.gui.events import UIOnChangeEvent -from arcade.gui.widgets.toggle import UITextureToggle - - -class MyView(View): - def __init__(self): - super().__init__() - self.ui = UIManager() - - on_texture = load_texture(":resources:gui_basic_assets/toggle/switch_green.png") - off_texture = load_texture(":resources:gui_basic_assets/toggle/switch_red.png") - self.toggle = UITextureToggle(on_texture=on_texture, off_texture=off_texture) - - # Add toggle to UIManager, use UIAnchorLayout to center on screen - self.ui.add(UIAnchorLayout(children=[self.toggle])) - - # Listen for value changes - @self.toggle.event("on_change") - def print_oon_change(event: UIOnChangeEvent): - print(f"New value {event.new_value}") - - def on_show_view(self): - self.window.background_color = arcade.color.DARK_BLUE_GRAY - # Enable UIManager when view is shown to catch window events - self.ui.enable() - - def on_hide_view(self): - self.ui.disable() - - def on_draw(self): - self.ui.draw() - - -if __name__ == "__main__": - window = arcade.Window(800, 600, "UIExample", resizable=True) - window.show_view(MyView()) - window.run() diff --git a/arcade/gui/examples/widget_gallery.py b/arcade/gui/examples/widget_gallery.py deleted file mode 100644 index eac7e7ff58..0000000000 --- a/arcade/gui/examples/widget_gallery.py +++ /dev/null @@ -1,194 +0,0 @@ -"""A combination of multiple widgets from other examples - -See the other GUI examples for more information. - -If arcade and Python are properly installed, you can run this example with: -python -m arcade.gui.examples.widget_gallery -""" - -from __future__ import annotations - -from textwrap import dedent - -import arcade -from arcade import load_texture -from arcade.gui import ( - UIManager, - UITextureButton, - UIGridLayout, - NinePatchTexture, - UILabel, - UITextureToggle, - UITextArea, - UIInputText, - UIBoxLayout, - UISlider, -) -from arcade.gui.examples.textured_slider import UITextureSlider -from arcade.gui.widgets.layout import UIAnchorLayout - -# Preload textures, because they are mostly used multiple times, so they are not -# loaded multiple times -TEX_RED_BUTTON_NORMAL = load_texture(":resources:gui_basic_assets/red_button_normal.png") -TEX_RED_BUTTON_HOVER = load_texture(":resources:gui_basic_assets/red_button_hover.png") -TEX_RED_BUTTON_PRESS = load_texture(":resources:gui_basic_assets/red_button_press.png") -TEX_SWITCH_RED = load_texture(":resources:gui_basic_assets/toggle/switch_red.png") -TEX_SWITCH_GREEN = load_texture(":resources:gui_basic_assets/toggle/switch_green.png") -TEX_SLIDER_THUMB = arcade.load_texture(":resources:gui_basic_assets/slider_thumb.png") -TEX_SLIDER_TRACK = arcade.load_texture(":resources:gui_basic_assets/slider_track.png") - - -class MyView(arcade.View): - def __init__(self): - super().__init__() - self.ui = UIManager() - - anchor = self.ui.add(UIAnchorLayout()) - grid = anchor.add( - UIGridLayout( - size_hint=(0.9, 0.9), - column_count=2, - row_count=7, - vertical_spacing=5, - horizontal_spacing=5, - ) - ) - - # Texture buttons using nine patch textures - grid.add( - col_num=0, - row_num=0, - child=UITextureButton( - texture=TEX_RED_BUTTON_NORMAL, - texture_hovered=TEX_RED_BUTTON_HOVER, - texture_pressed=TEX_RED_BUTTON_PRESS, - ), - ) - - # Texture buttons using nine patch textures - grid.add( - col_num=0, - row_num=1, - child=UITextureButton( - texture=NinePatchTexture( - left=5, - right=5, - bottom=5, - top=5, - texture=TEX_RED_BUTTON_NORMAL, - ), - texture_hovered=NinePatchTexture( - left=5, - right=5, - bottom=5, - top=5, - texture=TEX_RED_BUTTON_HOVER, - ), - texture_pressed=NinePatchTexture( - left=5, - right=5, - bottom=5, - top=5, - texture=TEX_RED_BUTTON_PRESS, - ), - ), - ) - - # Some text - grid.add( - col_num=0, - row_num=2, - child=UILabel(text="abcdefghijklmnopqrstuvwäöüABCDEFG"), - ) - - # Simple toggle - - toggles = grid.add( - UIBoxLayout(space_between=10, vertical=False), - col_num=0, - row_num=3, - ) - toggles.add( - UITextureToggle( - on_texture=TEX_SWITCH_GREEN, - off_texture=TEX_SWITCH_RED, - ) - ) - toggles.add( - UITextureToggle( - on_texture=TEX_SWITCH_GREEN, - off_texture=TEX_SWITCH_RED, - ) - ).disabled = True - - # Simple slider - grid.add( - col_num=0, - row_num=4, - child=UITextureSlider( - track=TEX_SLIDER_TRACK, - thumb=TEX_SLIDER_THUMB, - ), - ) - - # Scaled slider using ninepatch texture - grid.add( - col_num=0, - row_num=5, - child=UITextureSlider( - track=NinePatchTexture( - texture=TEX_SLIDER_TRACK, - left=30, - right=33, - bottom=18, - top=18, - ), - thumb=TEX_SLIDER_THUMB, - height=40, - ), - ) - - # Simple slider - grid.add( - col_num=0, - row_num=6, - child=UISlider(), - ) - - # Input text - grid.add(col_num=1, row_num=0, child=UIInputText(width=300).with_border()) - - example_text = dedent( - """ - Gamers can feel when developers are passionate about their games. - They can smell it like a dog smells fear. - Don't be afraid to hold onto your unique vision: - just be aware that it may not turn out exactly how you envisioned. - """ - ) - - grid.add( - col_num=1, - row_num=1, - row_span=2, - child=UITextArea(text=example_text, height=150).with_border(), - ) - - def on_show_view(self): - self.window.background_color = arcade.color.DARK_BLUE_GRAY - # Enable UIManager when view is shown to catch window events - self.ui.enable() - - def on_hide_view(self): - # Disable UIManager when view gets inactive - self.ui.disable() - - def on_draw(self): - self.clear() - self.ui.draw() - - -if __name__ == "__main__": - window = arcade.Window(800, 600, "UIExample", resizable=True) - window.show_view(MyView()) - window.run() diff --git a/arcade/gui/experimental/__init__.py b/arcade/gui/experimental/__init__.py index 447fdb2267..0b99ff244a 100644 --- a/arcade/gui/experimental/__init__.py +++ b/arcade/gui/experimental/__init__.py @@ -4,12 +4,10 @@ No Deprecation warnings are given for changes in this module. """ -from arcade.gui.examples.textured_slider import UITextureSlider from arcade.gui.experimental.scroll_area import UIScrollArea from arcade.gui.experimental.password_input import UIPasswordInput __all__ = [ - "UITextureSlider", "UIScrollArea", "UIPasswordInput", ] diff --git a/arcade/gui/experimental/password_input.py b/arcade/gui/experimental/password_input.py index d417488a1c..c0cbd3ecee 100644 --- a/arcade/gui/experimental/password_input.py +++ b/arcade/gui/experimental/password_input.py @@ -6,7 +6,12 @@ class UIPasswordInput(UIInputText): - """A password input field. The text is hidden with asterisks.""" + """A password input field. The text is hidden with asterisks. + + Hint: It is recommended to set a background color to prevent full render cycles + when the caret blinks. + + """ def on_event(self, event: UIEvent) -> Optional[bool]: """Remove new lines from the input, which are not allowed in passwords.""" diff --git a/arcade/gui/experimental/typed_text_input.py b/arcade/gui/experimental/typed_text_input.py index ff7c6e34b7..3d3c8d2b35 100644 --- a/arcade/gui/experimental/typed_text_input.py +++ b/arcade/gui/experimental/typed_text_input.py @@ -216,7 +216,7 @@ def __init__(self): right=5, top=5, bottom=5, - texture=arcade.load_texture(":resources:gui_basic_assets/window/grey_panel.png"), + texture=arcade.load_texture(":resources:gui_basic_assets/window/panel_gray.png"), ) self.instructions = UILabel( text="Valid float values -> black text\nNon-float values -> red text", diff --git a/arcade/gui/property.py b/arcade/gui/property.py index 10facc1b0a..87c8b63670 100644 --- a/arcade/gui/property.py +++ b/arcade/gui/property.py @@ -312,13 +312,13 @@ def __delitem__(self, key): self.dispatch() @override - def __iadd__(self, *args): # type: ignore + def __iadd__(self, *args): list.__iadd__(self, *args) self.dispatch() return self @override - def __imul__(self, *args): # type: ignore + def __imul__(self, *args): list.__imul__(self, *args) self.dispatch() return self @@ -373,7 +373,7 @@ def reverse(self): self.dispatch() -class ListProperty(Property): +class ListProperty(Property, Generic[P]): """Property that represents a list. Only list are allowed. Any other classes are forbidden. @@ -383,7 +383,7 @@ def __init__(self): super().__init__(default_factory=_ObservableList) @override - def set(self, instance, value: dict): + def set(self, instance, value: list): """Set value for owner instance, wraps the list into an observable list.""" - value = _ObservableList(self, instance, value) # type: ignore + value = _ObservableList(self, instance, value) super().set(instance, value) diff --git a/arcade/gui/surface.py b/arcade/gui/surface.py index 8ecb3a215d..4f2f17b5c0 100644 --- a/arcade/gui/surface.py +++ b/arcade/gui/surface.py @@ -197,11 +197,12 @@ def limit(self, rect: Rect): w = max(w, 1) h = max(h, 1) + # round to nearest pixel, to avoid off by 1-pixel errors in ui viewport_rect = LBWH( - int(l * self._pixel_ratio), - int(b * self._pixel_ratio), - int(w * self._pixel_ratio), - int(h * self._pixel_ratio), + round(l * self._pixel_ratio), + round(b * self._pixel_ratio), + round(w * self._pixel_ratio), + round(h * self._pixel_ratio), ) self.fbo.viewport = viewport_rect.viewport diff --git a/arcade/gui/ui_manager.py b/arcade/gui/ui_manager.py index 821cab46eb..676018cea0 100644 --- a/arcade/gui/ui_manager.py +++ b/arcade/gui/ui_manager.py @@ -405,11 +405,11 @@ def on_mouse_scroll(self, x, y, scroll_x, scroll_y): def on_key_press(self, symbol: int, modifiers: int): """Converts key press event to UI event and dispatches it.""" - return self.dispatch_ui_event(UIKeyPressEvent(self, symbol, modifiers)) # type: ignore + return self.dispatch_ui_event(UIKeyPressEvent(self, symbol, modifiers)) def on_key_release(self, symbol: int, modifiers: int): """Converts key release event to UI event and dispatches it.""" - return self.dispatch_ui_event(UIKeyReleaseEvent(self, symbol, modifiers)) # type: ignore + return self.dispatch_ui_event(UIKeyReleaseEvent(self, symbol, modifiers)) def on_text(self, text): """Converts text event to UI event and dispatches it.""" @@ -442,7 +442,7 @@ def on_resize(self, width, height): self.trigger_render() @property - def rect(self) -> Rect: # type: ignore + def rect(self) -> Rect: """The rect of the UIManager, which is the window size.""" return LBWH(0, 0, *self.window.get_size()) diff --git a/arcade/gui/view.py b/arcade/gui/view.py index a93c4e3d85..a4a7d52af1 100644 --- a/arcade/gui/view.py +++ b/arcade/gui/view.py @@ -1,5 +1,11 @@ +from typing import TypeVar + from arcade import View -from arcade.gui import UIManager +from arcade.gui.ui_manager import UIManager +from arcade.gui.widgets import UIWidget +from arcade.types import RGBOrA255 + +W = TypeVar("W", bound=UIWidget) class UIView(View): @@ -11,6 +17,9 @@ class UIView(View): This class provides two draw callbacks: on_draw_before_ui and on_draw_after_ui. Use these to draw custom elements before or after the UI elements are drawn. + The screen is cleared before on_draw_before_ui is called + with the background color of the window. + If you override ``on_show_view`` or ``on_show_view``, don't forget to call super().on_show_view() or super().on_hide_view(). @@ -19,6 +28,11 @@ class UIView(View): def __init__(self): super().__init__() self.ui = UIManager() + self.background_color: RGBOrA255 | None = None + + def add_widget(self, widget: W) -> W: + """Add a widget to the UIManager of this view.""" + return self.ui.add(widget) def on_show_view(self): """If subclassing UIView, don't forget to call super().on_show_view().""" @@ -32,7 +46,7 @@ def on_draw(self): """To subclass UIView and add custom drawing, override on_draw_before_ui and on_draw_after_ui. """ - self.clear() + self.clear(color=self.background_color) self.on_draw_before_ui() self.ui.draw() self.on_draw_after_ui() diff --git a/arcade/gui/widgets/__init__.py b/arcade/gui/widgets/__init__.py index dbb289eeb2..c0f6f3ed55 100644 --- a/arcade/gui/widgets/__init__.py +++ b/arcade/gui/widgets/__init__.py @@ -60,7 +60,7 @@ class UIWidget(EventDispatcher, ABC): rect: Rect = Property(LBWH(0, 0, 1, 1)) # type: ignore visible: bool = Property(True) # type: ignore - size_hint: Optional[Tuple[float, float]] = Property(None) # type: ignore + size_hint: Optional[Tuple[float | None, float | None]] = Property(None) # type: ignore size_hint_min: Optional[Tuple[float, float]] = Property(None) # type: ignore size_hint_max: Optional[Tuple[float, float]] = Property(None) # type: ignore @@ -83,9 +83,9 @@ def __init__( height: float = 100, children: Iterable["UIWidget"] = tuple(), # Properties which might be used by layouts - size_hint=None, # in percentage - size_hint_min=None, # in pixel - size_hint_max=None, # in pixel + size_hint: Optional[Tuple[float | None, float | None]] = None, # in percentage + size_hint_min: Optional[Tuple[float, float]] = None, # in pixel + size_hint_max: Optional[Tuple[float, float]] = None, # in pixel **kwargs, ): self._requires_render = True @@ -97,8 +97,8 @@ def __init__( self.size_hint_min = size_hint_min self.size_hint_max = size_hint_max - self.register_event_type("on_event") # type: ignore # https://github.com/pyglet/pyglet/pull/1173 # noqa - self.register_event_type("on_update") # type: ignore # https://github.com/pyglet/pyglet/pull/1173 # noqa + self.register_event_type("on_event") + self.register_event_type("on_update") for child in children: self.add(child) @@ -510,6 +510,9 @@ def center_on_screen(self: W) -> W: self.rect = self.rect.align_center(center) return self + def __repr__(self): + return f"<{self.__class__.__name__} {self.rect.lbwh}>" + class UIInteractiveWidget(UIWidget): """Base class for widgets which use mouse interaction (hover, pressed, clicked) @@ -540,8 +543,8 @@ def __init__( *, x: float = 0, y: float = 0, - width: float, - height: float, + width: float = 100, + height: float = 100, size_hint=None, size_hint_min=None, size_hint_max=None, @@ -558,7 +561,7 @@ def __init__( size_hint_max=size_hint_max, **kwargs, ) - self.register_event_type("on_click") # type: ignore + self.register_event_type("on_click") self.interaction_buttons = interaction_buttons @@ -785,8 +788,9 @@ def _do_layout(self): super()._do_layout() def do_layout(self): - """Triggered by the UIManager before rendering, :class:`UILayout` s should place - themselves and/or children. Do layout will be triggered on children afterward. + """do_layout is triggered by the UIManager before rendering. + :class:`UILayout` should position their children. + Afterward, do_layout of child widgets will be triggered. Use :meth:`UIWidget.trigger_render` to trigger a rendering before the next frame, this will happen automatically if the position or size of this widget changed. diff --git a/arcade/gui/widgets/buttons.py b/arcade/gui/widgets/buttons.py index 3cd10a5af5..76fc6d2216 100644 --- a/arcade/gui/widgets/buttons.py +++ b/arcade/gui/widgets/buttons.py @@ -4,7 +4,7 @@ from typing import Optional, Union import arcade -from arcade import Texture +from arcade import Texture, color, uicolor from arcade.gui.nine_patch import NinePatchTexture from arcade.gui.property import DictProperty, bind from arcade.gui.style import UIStyleBase, UIStyledWidget @@ -25,9 +25,8 @@ class UITextureButtonStyle(UIStyleBase): """ font_size: int = 12 - font_name: FontNameOrNames = ("calibri", "arial") - font_color: RGBA255 = arcade.color.WHITE - border_width: int = 2 + font_name: FontNameOrNames = ("Kenney Future", "arial", "calibri") + font_color: RGBA255 = uicolor.WHITE_CLOUDS class UITextureButton(UIInteractiveWidget, UIStyledWidget[UITextureButtonStyle], UITextWidget): @@ -64,22 +63,13 @@ class UITextureButton(UIInteractiveWidget, UIStyledWidget[UITextureButtonStyle], DEFAULT_STYLE = { "normal": UIStyle(), "hover": UIStyle( - font_size=12, - font_name=("calibri", "arial"), - font_color=arcade.color.WHITE, - border_width=2, + font_color=uicolor.WHITE_CLOUDS, ), "press": UIStyle( - font_size=12, - font_name=("calibri", "arial"), - font_color=arcade.color.BLACK, - border_width=2, + font_color=uicolor.DARK_BLUE_MIDNIGHT_BLUE, ), "disabled": UIStyle( - font_size=12, - font_name=("calibri", "arial"), - font_color=arcade.color.WHITE, - border_width=2, + font_color=uicolor.WHITE_SILVER, ), } @@ -240,38 +230,68 @@ class UIStyle(UIStyleBase): """ font_size: int = 12 - font_name: FontNameOrNames = ("calibri", "arial") - font_color: RGBA255 = arcade.color.WHITE - bg: RGBA255 = (21, 19, 21, 255) + font_name: FontNameOrNames = ("Kenney Future", "arial", "calibri") + font_color: RGBA255 = color.WHITE + bg: RGBA255 = uicolor.DARK_BLUE_MIDNIGHT_BLUE border: Optional[RGBA255] = None border_width: int = 0 DEFAULT_STYLE = { "normal": UIStyle(), "hover": UIStyle( - font_size=12, - font_name=("calibri", "arial"), - font_color=arcade.color.WHITE, - bg=(21, 19, 21, 255), - border=(77, 81, 87, 255), + font_color=color.WHITE, + bg=uicolor.DARK_BLUE_WET_ASPHALT, + border=uicolor.GRAY_CONCRETE, + ), + "press": UIStyle( + font_color=uicolor.DARK_BLUE_MIDNIGHT_BLUE, + bg=uicolor.WHITE_CLOUDS, + border=uicolor.GRAY_CONCRETE, + ), + "disabled": UIStyle( + font_color=uicolor.WHITE_SILVER, + bg=uicolor.GRAY_ASBESTOS, + ), + } + + STYLE_RED = { + "normal": UIStyle( + font_color=uicolor.WHITE_CLOUDS, + bg=uicolor.RED_ALIZARIN, + border=uicolor.RED_POMEGRANATE, + ), + "hover": UIStyle( + bg=uicolor.RED_ALIZARIN, + font_color=uicolor.WHITE_CLOUDS, + border=uicolor.WHITE_SILVER, border_width=2, ), "press": UIStyle( - font_size=12, - font_name=("calibri", "arial"), - font_color=arcade.color.BLACK, - bg=arcade.color.WHITE, - border=arcade.color.WHITE, + bg=uicolor.RED_POMEGRANATE, + font_color=uicolor.WHITE_CLOUDS, + border=uicolor.WHITE_SILVER, border_width=2, ), "disabled": UIStyle( - font_size=12, - font_name=("calibri", "arial"), - font_color=arcade.color.WHITE, - bg=arcade.color.GRAY, - border=None, + bg=uicolor.GRAY_ASBESTOS, + ), + } + + STYLE_BLUE = { + "normal": UIStyle(bg=uicolor.BLUE_PETER_RIVER, font_color=uicolor.WHITE_CLOUDS), + "hover": UIStyle( + bg=uicolor.BLUE_BELIZE_HOLE, + font_color=uicolor.WHITE_CLOUDS, + border=uicolor.WHITE_SILVER, + border_width=2, + ), + "press": UIStyle( + bg=uicolor.DARK_BLUE_MIDNIGHT_BLUE, + font_color=uicolor.WHITE_CLOUDS, + border=uicolor.WHITE_SILVER, border_width=2, ), + "disabled": UIStyle(bg=uicolor.GRAY_ASBESTOS), } def __init__( @@ -318,6 +338,8 @@ def do_render(self, surface: Surface): """Render a flat button, graphical representation depends on the current state.""" self.prepare_render(surface) style: UIFlatButton.UIStyle = self.get_current_style() + if style is None: + raise ValueError(f"No style found for state {self.get_current_state()}") # update label # this might trigger another render run, due to label size change diff --git a/arcade/gui/widgets/dropdown.py b/arcade/gui/widgets/dropdown.py index 08e37c1521..90e5fbe5d8 100644 --- a/arcade/gui/widgets/dropdown.py +++ b/arcade/gui/widgets/dropdown.py @@ -6,6 +6,7 @@ from pyglet.event import EVENT_HANDLED import arcade +from arcade import uicolor from arcade.gui import UIEvent, UIMousePressEvent from arcade.gui.events import UIOnChangeEvent, UIOnClickEvent from arcade.gui.ui_manager import UIManager @@ -58,7 +59,6 @@ def on_change(event: UIOnChangeEvent): height: Height of each of the option. default: The default value shown. options: The options displayed when the layout is clicked. - style: Used to style the dropdown. """ DIVIDER = None @@ -68,27 +68,25 @@ def __init__( *, x: float = 0, y: float = 0, - width: float = 100, - height: float = 20, + width: float = 150, + height: float = 30, default: Optional[str] = None, options: Optional[list[Union[str, None]]] = None, - style=None, **kwargs, ): - if style is None: - style = {} - # TODO handle if default value not in options or options empty if options is None: options = [] self._options = options self._value = default - super().__init__(x=x, y=y, width=width, height=height, style=style, **kwargs) + super().__init__(x=x, y=y, width=width, height=height, **kwargs) # Setup button showing value + style = deepcopy(UIFlatButton.DEFAULT_STYLE) + style["hover"].font_color = uicolor.GREEN_NEPHRITIS self._default_button = UIFlatButton( - text=self._value or "", width=self.width, height=self.height + text=self._value or "", width=self.width, height=self.height, style=style ) self._default_button.on_click = self._on_button_click # type: ignore @@ -99,7 +97,7 @@ def __init__( # add children after super class setup self.add(self._default_button) - self.register_event_type("on_change") # type: ignore # https://github.com/pyglet/pyglet/pull/1173 # noqa + self.register_event_type("on_change") self.with_border(color=arcade.color.RED) @@ -126,7 +124,7 @@ def _update_options(self): # is there another way then deepcopy, does it matter? # ("premature optimization is the root of all evil") active_style = deepcopy(UIFlatButton.DEFAULT_STYLE) - active_style["normal"]["bg"] = (55, 66, 81) + active_style["normal"]["bg"] = uicolor.GREEN_NEPHRITIS for option in self._options: if option is None: # None = UIDropdown.DIVIDER, required by pyright diff --git a/arcade/gui/widgets/layout.py b/arcade/gui/widgets/layout.py index 9b1554b28b..b8c715ee2e 100644 --- a/arcade/gui/widgets/layout.py +++ b/arcade/gui/widgets/layout.py @@ -1,11 +1,12 @@ from __future__ import annotations -from typing import Iterable, Optional, TypeVar, cast +from dataclasses import dataclass +from typing import Dict, Iterable, List, Optional, Tuple, TypeVar -from typing_extensions import override +from typing_extensions import Literal, override from arcade.gui.property import bind, unbind -from arcade.gui.widgets import UILayout, UIWidget +from arcade.gui.widgets import UILayout, UIWidget, _ChildEntry __all__ = ["UILayout", "UIAnchorLayout", "UIBoxLayout", "UIGridLayout"] @@ -60,6 +61,10 @@ class UIAnchorLayout(UILayout): default_anchor_x = "center" default_anchor_y = "center" + _restrict_child_size = False + """Whether to restrict the child size to the layout size. + For scroll use cases this is not wanted, but for UITextWidgets it is. + """ def __init__( self, @@ -157,6 +162,9 @@ def _place_child( if shmx_w: new_child_rect = new_child_rect.max_size(width=shmx_w) + if self._restrict_child_size: + new_child_rect = new_child_rect.max_size(width=self.content_width) + if sh_h is not None: new_child_rect = new_child_rect.resize(height=self.content_height * sh_h) @@ -165,8 +173,8 @@ def _place_child( if shmx_h: new_child_rect = new_child_rect.max_size(height=shmx_h) - # Stay in bounds - new_child_rect = new_child_rect.max_size(*self.content_size) + if self._restrict_child_size: + new_child_rect = new_child_rect.max_size(height=self.content_height) # Calculate position content_rect = self.content_rect @@ -350,51 +358,43 @@ def do_layout(self): if not self.children: return - if self.vertical: - available_width = self.content_width - - # Determine if some space is available for children to grow - available_height = max(0, self.height - self.size_hint_min[1]) - total_size_hint_height = ( - sum(child.size_hint[1] or 0 for child in self.children if child.size_hint) or 1 - ) # Prevent division by zero - - for child in self.children: - new_rect = child.rect - - # Collect all size hints - sh_w, sh_h = child.size_hint or (None, None) - shmn_w, shmn_h = child.size_hint_min or (None, None) - shmx_w, shmx_h = child.size_hint_max or (None, None) + # main axis + constraints = [ + _C.from_widget_height(child) if self.vertical else _C.from_widget_width(child) + for child, _ in self._children + ] - # Apply y-axis - if sh_h is not None: - min_height_value = shmn_h or 0 - - # Maximal growth to parent.width * shw - available_growth_height = min_height_value + available_height * ( - sh_h / total_size_hint_height - ) - max_growth_height = self.height * sh_h - new_rect = new_rect.resize( - height=min(available_growth_height, max_growth_height) - ) - - if shmn_h is not None: - new_rect = new_rect.min_size(height=shmn_h) - if shmx_h is not None: - new_rect = new_rect.max_size(height=shmx_h) + available_space = ( + self.content_height if self.vertical else self.content_width + ) - self._space_between * (len(self.children) - 1) + main_sizes = _box_axis_algorithm(constraints, available_space) - # Apply x-axis - if sh_w is not None: - new_rect = new_rect.resize(width=max(available_width * sh_w, shmn_w or 0)) + # orthogonal axis + constraints = [ + _C.from_widget_width(child) if self.vertical else _C.from_widget_height(child) + for child, _ in self._children + ] + orthogonal_sizes = _box_orthogonal_algorithm( + constraints, self.content_width if self.vertical else self.content_height + ) - if shmn_w is not None: - new_rect = new_rect.min_size(width=shmn_w) - if shmx_w is not None: - new_rect = new_rect.max_size(width=shmx_w) + for (child, data), main_size, ortho_size in zip( + self._children, main_sizes, orthogonal_sizes + ): + # apply calculated sizes, condition regarding existing size_hint + # are already covered in calculation input + new_rect = child.rect.resize( + height=main_size if self.vertical else ortho_size, + width=ortho_size if self.vertical else main_size, + ) + new_rect = ( + new_rect.align_x(start_x + self.content_width / 2) + if self.vertical + else new_rect.align_y(start_y - self.content_height // 2) + ) - # Align the children + # align on main and orthogonal axis and update start position + if self.vertical: if self.align == "left": new_rect = new_rect.align_left(start_x) elif self.align == "right": @@ -402,99 +402,48 @@ def do_layout(self): else: center_x = start_x + self.content_width // 2 new_rect = new_rect.align_x(center_x) - new_rect = new_rect.align_top(start_y) - child.rect = new_rect - - start_y -= child.height - start_y -= self._space_between - else: - center_y = start_y - self.content_height // 2 - - available_height = self.content_height - - # Calculate if some space is available for children to grow. - available_width = max(0, self.width - self.size_hint_min[0]) - total_size_hint_width = ( - sum(child.size_hint[0] or 0 for child in self.children if child.size_hint) or 1 - ) # Prevent division by zero - - # 0. check if any hint given, if not, continue with step 4. - # 1. change size to minimal - # 2. grow using size_hint - # 3. ensure size_hint_max - # 4. place child - - for child in self.children: - new_rect = child.rect - - # Collect all size hints - sh_w, sh_h = child.size_hint or (None, None) - shmn_w, shmn_h = child.size_hint_min or (None, None) - shmx_w, shmx_h = child.size_hint_max or (None, None) - - # Apply x-axis - if sh_w is not None: - min_width_value = shmn_w or 0 - - # Maximal growth to parent.width * shw - available_growth_width = min_width_value + available_width * ( - sh_w / total_size_hint_width - ) - max_growth_width = self.width * sh_w - new_rect = new_rect.resize( - width=min( - available_growth_width, max_growth_width - ) # This does not enforce the minimum width - ) - - if shmn_w is not None: - new_rect = new_rect.min_size(width=shmn_w) - - if shmx_w is not None: - new_rect = new_rect.max_size(width=shmx_w) - - # Apply vertical axis - if sh_h is not None: - new_rect = new_rect.resize(height=max(available_height * sh_h, shmn_h or 0)) - - if shmn_h is not None: - new_rect = new_rect.min_size(height=shmn_h) - - if shmx_h is not None: - new_rect = new_rect.max_size(height=shmx_h) - - # Align all children + start_y -= main_size + self._space_between + else: if self.align == "top": new_rect = new_rect.align_top(start_y) elif self.align == "bottom": new_rect = new_rect.align_bottom(start_y - self.content_height) else: + center_y = start_y - self.content_height // 2 new_rect = new_rect.align_y(center_y) - new_rect = new_rect.align_left(start_x) - child.rect = new_rect + start_x += main_size + self._space_between - start_x += child.width - start_x += self._space_between + # update child rect + child.rect = new_rect class UIGridLayout(UILayout): - """Place widgets in a grid layout. This is similar to tkinter's ``grid`` - layout geometry manager. + """Place widgets in a grid. - Defaults to ``size_hint = (0, 0)``. + Widgets can span multiple columns and rows. + By default, the layout will only use the minimal required space (``size_hint = (0, 0)``). - Supports the options ``size_hint``, ``size_hint_min``, and - ``size_hint_max``. + Widgets can provide a ``size_hint`` to request dynamic space relative to the layout size. + A size_hint of ``(1, 1)`` will fill the available space, while ``(0.1, 0.1)`` + will use maximum 10% of the layouts total size. Children are resized based on ``size_hint``. Maximum and minimum - ``size_hint``s only take effect if a ``size_hint`` is given. ``size_hint_min`` is automatically - updated based on the minimal required space by children. + ``size_hint``s only take effect if a ``size_hint`` is given. + + The layouts ``size_hint_min`` is automatically + updated based on the minimal required space by children, after layouting. + + The width of columns and height of rows are calculated based on the size hints of the children. + The highest size_hint_min of a child in a column or row is used. If a child has no size_hint, + the actual size is considered. Args: x: ``x`` coordinate of bottom left corner. y: ``y`` coordinate of bottom left corner. + width: Width of the layout. + height: Height of the layout. align_horizontal: Align children in orthogonal direction. Options include ``left``, ``center``, and ``right``. align_vertical: Align children in orthogonal direction. Options @@ -516,6 +465,8 @@ def __init__( *, x=0, y=0, + width=1, + height=1, align_horizontal="center", align_vertical="center", children: Iterable[UIWidget] = tuple(), @@ -525,18 +476,16 @@ def __init__( vertical_spacing: int = 0, column_count: int = 1, row_count: int = 1, - style=None, **kwargs, ): super(UIGridLayout, self).__init__( x=x, y=y, - width=1, - height=1, + width=width, + height=height, children=children, size_hint=size_hint, size_hint_max=size_hint_max, - style=style, **kwargs, ) self._size_hint_requires_update = True @@ -549,23 +498,24 @@ def __init__( self.align_horizontal = align_horizontal self.align_vertical = align_vertical - bind(self, "_children", self._update_size_hints) - bind(self, "_border_width", self._update_size_hints) + bind(self, "_children", self._trigger_size_hint_update) + bind(self, "_border_width", self._trigger_size_hint_update) - bind(self, "_padding_left", self._update_size_hints) - bind(self, "_padding_right", self._update_size_hints) - bind(self, "_padding_top", self._update_size_hints) - bind(self, "_padding_bottom", self._update_size_hints) + bind(self, "_padding_left", self._trigger_size_hint_update) + bind(self, "_padding_right", self._trigger_size_hint_update) + bind(self, "_padding_top", self._trigger_size_hint_update) + bind(self, "_padding_bottom", self._trigger_size_hint_update) # initially update size hints + # TODO is this required? self._update_size_hints() def add( self, child: W, - col_num: int = 0, - row_num: int = 0, - col_span: int = 1, + column: int = 0, + row: int = 0, + column_span: int = 1, row_span: int = 1, **kwargs, ) -> W: @@ -573,11 +523,11 @@ def add( Args: child: Specified widget to add as a child of the layout. - col_num: Column index in which the widget is to be added. + column: Column index in which the widget is to be added. The first column at the left of the widget starts at 0. - row_num: The row number in which the widget is to be added. + row: The row number in which the widget is to be added. The first row at the top of the layout is numbered 0. - col_span: Number of columns the widget will stretch for. + column_span: Number of columns the widget will stretch for. row_span: Number of rows the widget will stretch for. """ # subscribe to child's changes, which might affect the own size hint @@ -589,9 +539,9 @@ def add( return super().add( child, - col_num=col_num, - row_num=row_num, - col_span=col_span, + column=column, + row=row, + column_span=column_span, row_span=row_span, **kwargs, ) @@ -620,197 +570,369 @@ def prepare_layout(self): def _update_size_hints(self): self._size_hint_requires_update = False - max_width_per_column: list[list[tuple[int, int]]] = [ - [(0, 1) for _ in range(self.row_count)] for _ in range(self.column_count) - ] - max_height_per_row: list[list[tuple[int, int]]] = [ - [(0, 1) for _ in range(self.column_count)] for _ in range(self.row_count) - ] - - for child, data in self._children: - col_num = data["col_num"] - row_num = data["row_num"] - col_span = data["col_span"] - row_span = data["row_span"] - - shmn_w, shmn_h = UILayout.min_size_of(child) - - for i in range(col_num, col_span + col_num): - max_width_per_column[i][row_num] = (0, 0) - - max_width_per_column[col_num][row_num] = (shmn_w, col_span) - - for i in range(row_num, row_span + row_num): - max_height_per_row[i][col_num] = (0, 0) - - max_height_per_row[row_num][col_num] = (shmn_h, row_span) - - principal_width_ratio_list = [] - principal_height_ratio_list = [] - - for row in max_height_per_row: - principal_height_ratio_list.append(max(height / (span or 1) for height, span in row)) + if not self.children: + self.size_hint_min = (0, 0) + return - for col in max_width_per_column: - principal_width_ratio_list.append(max(width / (span or 1) for width, span in col)) + # 0. generate list for all rows and columns + columns = [] + for i in range(self.column_count): + columns.append([]) + rows = [] + for i in range(self.row_count): + rows.append([]) + + for entry in self._children: + col_num = entry.data["column"] + row_num = entry.data["row"] + col_span = entry.data["column_span"] + row_span = entry.data["row_span"] + + # we put the entry in all columns and rows it spans + for c in range(col_span): + columns[col_num + c].append(entry) + + for r in range(row_span): + rows[row_num + r].append(entry) + + # 1.a per column, collect max of size_hint_min and max size_hint + minimal_width_per_column = [] + for col in columns: + min_width = 0 + max_sh = 0 + for entry in col: + col_span = entry.data["column_span"] + # if the cell spans multiple columns, + # we need to reduce the minimal required width by the horizontal spacing + consumed_space = self._horizontal_spacing if col_span > 1 else 0 + + min_w, _ = UILayout.min_size_of(entry.child) + min_width = max(min_width, min_w / col_span - consumed_space) + + shw, _ = entry.child.size_hint or (0, 0) + max_sh = max(max_sh, shw) if shw else max_sh + + minimal_width_per_column.append(min_width) + + # 1.b per row, collect max of size_hint_min and max size_hint + minimal_height_per_row = [] + for row in rows: + min_height = 0 + max_sh = 0 + for entry in row: + row_span = entry.data["row_span"] + # if the cell spans multiple rows, + # we need to reduce the minimal required height by the vertical spacing + consumed_space = self._vertical_spacing if row_span > 1 else 0 + + _, min_h = UILayout.min_size_of(entry.child) + min_height = max(min_height, min_h / row_span - consumed_space) + + _, shh = entry.child.size_hint or (0, 0) + max_sh = max(max_sh, shh) if shh else max_sh + + minimal_height_per_row.append(min_height) base_width = self._padding_left + self._padding_right + 2 * self._border_width base_height = self._padding_top + self._padding_bottom + 2 * self._border_width - content_height = sum(principal_height_ratio_list) + self.row_count * self._vertical_spacing - content_width = ( - sum(principal_width_ratio_list) + self.column_count * self._horizontal_spacing + self.size_hint_min = ( + base_width + + sum(minimal_width_per_column) + + (self.column_count - 1) * self._horizontal_spacing, + base_height + + sum(minimal_height_per_row) + + (self.row_count - 1) * self._vertical_spacing, ) - self.size_hint_min = (base_width + content_width, base_height + content_height) - def do_layout(self): """Executes the layout algorithm. - Children are placed in a grid layout based on the size hints.""" - initial_left_x = self.content_rect.left - start_y = self.content_rect.top + Children are placed in a grid layout based on the size hints. + Algorithm + --------- + + 0. generate list for all rows and columns + 1. per column, collect max of size_hint_min and max size_hint (widths) + 2. per row, collect max of size_hint_min and max size_hint (heights) + 3. use box layout algorithm to distribute space + 4. place widgets in grid layout + + """ + + # skip if no children if not self.children: return - child_sorted_row_wise = cast( - list[list[UIWidget]], - [[None for _ in range(self.column_count)] for _ in range(self.row_count)], + # 0. generate list for all rows and columns + columns = [] + for i in range(self.column_count): + columns.append([]) + rows = [] + for i in range(self.row_count): + rows.append([]) + + lookup: Dict[Tuple[int, int], _ChildEntry] = {} + for entry in self._children: + col_num = entry.data["column"] + row_num = entry.data["row"] + col_span = entry.data["column_span"] + row_span = entry.data["row_span"] + + # we put the entry in all columns and rows it spans + for c in range(col_span): + columns[col_num + c].append(entry) + + for r in range(row_span): + rows[row_num + r].append(entry) + + lookup[(col_num, row_num)] = entry + + # 1.a per column, collect max of size_hint_min and max size_hint + minimal_width_per_column = [] + max_size_hint_per_column = [] + for col in columns: + min_width = 0 + max_sh = 0 + for entry in col: + col_span = entry.data["column_span"] + # if the cell spans multiple columns, + # we need to reduce the minimal required width by the horizontal spacing + consumed_space = self._horizontal_spacing if col_span > 1 else 0 + + min_w, _ = UILayout.min_size_of(entry.child) + min_width = max(min_width, min_w / col_span - consumed_space) + + shw, _ = entry.child.size_hint or (0, 0) + max_sh = max(max_sh, shw) if shw else max_sh + + minimal_width_per_column.append(min_width) + max_size_hint_per_column.append(max_sh) + + # 1.b per row, collect max of size_hint_min and max size_hint + minimal_height_per_row = [] + max_size_hint_per_row = [] + for row in rows: + min_height = 0 + max_sh = 0 + for entry in row: + row_span = entry.data["row_span"] + # if the cell spans multiple rows, + # we need to reduce the minimal required height by the vertical spacing + consumed_space = self._vertical_spacing if row_span > 1 else 0 + + _, min_h = UILayout.min_size_of(entry.child) + min_height = max(min_height, min_h / row_span - consumed_space) + + _, shh = entry.child.size_hint or (0, 0) + max_sh = max(max_sh, shh) if shh else max_sh + + minimal_height_per_row.append(min_height) + max_size_hint_per_row.append(max_sh) + + # 2. use box layout algorithm to distribute space + column_constraints = [ + _C(minimal_width_per_column[i], None, max_size_hint_per_column[i]) + for i in range(self.column_count) + ] + column_sizes = _box_axis_algorithm( + column_constraints, + self.content_width - (self.column_count - 1) * self._horizontal_spacing, ) - max_width_per_column: list[list[tuple[float, int]]] = [ - [(0, 1) for _ in range(self.row_count)] for _ in range(self.column_count) - ] - max_height_per_row: list[list[tuple[float, int]]] = [ - [(0, 1) for _ in range(self.column_count)] for _ in range(self.row_count) + row_constraints = [ + _C(minimal_height_per_row[i], None, max_size_hint_per_row[i]) + for i in range(self.row_count) ] + row_sizes = _box_axis_algorithm( + row_constraints, self.content_height - (self.row_count - 1) * self._vertical_spacing + ) - for child, data in self._children: - col_num = data["col_num"] - row_num = data["row_num"] - col_span = data["col_span"] - row_span = data["row_span"] + # 3. place widgets in grid layout + start_y = self.content_rect.top + for row_num in range(self.row_count): + start_x = self.content_rect.left + for col_num in range(self.column_count): + entry = lookup.get((col_num, row_num)) + if not entry: + # still need to update start_x + start_x += column_sizes[col_num] + self._horizontal_spacing + continue + + # TODO handle row_span and col_span + child = entry.child + new_rect = child.rect - for i in range(col_num, col_span + col_num): - max_width_per_column[i][row_num] = (0, 0) + # combine size of cells this entry spans and add spacing + column_span = entry.data["column_span"] + cell_width: float = sum(column_sizes[col_num : col_num + column_span]) + cell_width += (column_span - 1) * self._horizontal_spacing - max_width_per_column[col_num][row_num] = (child.width, col_span) + row_span = entry.data["row_span"] + cell_height: float = sum(row_sizes[row_num : row_num + row_span]) + cell_height += (row_span - 1) * self._vertical_spacing - for i in range(row_num, row_span + row_num): - max_height_per_row[i][col_num] = (0, 0) + # apply calculated sizes, when size_hint is given + shw, shh = child.size_hint or (None, None) + shmn_w, shmn_h = child.size_hint_min or (None, None) + shmx_w, shmx_h = child.size_hint_max or (None, None) - max_height_per_row[row_num][col_num] = (child.height, row_span) + new_width = child.width + if shw is not None: + new_width = min(cell_width, shw * self.content_width) + new_width = max(new_width, shmn_w or 0) + if shmx_w is not None: + new_width = min(new_width, shmx_w) - for row in child_sorted_row_wise[row_num : row_num + row_span]: - row[col_num : col_num + col_span] = [child] * col_span + new_height = child.height + if shh is not None: + new_height = min(cell_height, shh * self.content_height) + new_height = max(new_height, shmn_h or 0) + if shmx_h is not None: + new_height = min(new_height, shmx_h) - principal_height_ratio_list = [] - principal_width_ratio_list = [] + new_rect = new_rect.resize(width=new_width, height=new_height) - # Making cell height same for each row. - for row in max_height_per_row: - principal_height_ratio = max( - (height + self._vertical_spacing) / (span or 1) for height, span in row - ) - principal_height_ratio_list.append(principal_height_ratio) - for i, (height, span) in enumerate(row): - if (height + self._vertical_spacing) / (span or 1) < principal_height_ratio: - row[i] = (principal_height_ratio * span, span) - - # Making cell width same for each column. - for col in max_width_per_column: - principal_width_ratio = max( - (width + self._horizontal_spacing) / (span or 1) for width, span in col - ) - principal_width_ratio_list.append(principal_width_ratio) - for i, (width, span) in enumerate(col): - if (width + self._horizontal_spacing) / (span or 1) < principal_width_ratio: - col[i] = (principal_width_ratio * span, span) - - content_height = sum(principal_height_ratio_list) + self.row_count * self._vertical_spacing - content_width = ( - sum(principal_width_ratio_list) + self.column_count * self._horizontal_spacing - ) + # align within cell + center_y = start_y - (cell_height / 2) + center_x = start_x + (cell_width / 2) - def ratio(dimensions: list) -> list: - """Used to calculate ratio of the elements based on the minimum value in the parameter. + if self.align_vertical == "top": + new_rect = new_rect.align_top(start_y) + elif self.align_vertical == "bottom": + new_rect = new_rect.align_bottom(start_y - row_sizes[row_num]) + else: + new_rect = new_rect.align_y(center_y) - Args: - dimension: List containing max height or width of the - cells. - """ - ratio_value = sum(dimensions) or 1 - return [dimension / ratio_value for dimension in dimensions] + if self.align_horizontal == "left": + new_rect = new_rect.align_left(start_x) + elif self.align_horizontal == "right": + new_rect = new_rect.align_right(start_x + cell_width) + else: + new_rect = new_rect.align_x(center_x) - expandable_height_ratio = ratio(principal_width_ratio_list) - expandable_width_ratio = ratio(principal_height_ratio_list) + # update child rect + child.rect = new_rect - total_available_height = self.content_rect.top - content_height - self.content_rect.bottom - total_available_width = self.content_rect.right - content_width - self.content_rect.left + start_x += column_sizes[col_num] + self._horizontal_spacing + start_y -= row_sizes[row_num] + self._vertical_spacing - # Row wise rendering children - for row_num, row in enumerate(child_sorted_row_wise): - max_height_row = 0 - start_x = initial_left_x - for col_num, child in enumerate(row): - constant_height = max_height_per_row[row_num][col_num][0] - height_expand_ratio = expandable_height_ratio[col_num] - available_height = constant_height + total_available_height * height_expand_ratio +@dataclass +class _C: + """Constrain values for the box algorithm. - constant_width = max_width_per_column[col_num][row_num][0] - width_expand_ratio = expandable_width_ratio[row_num] - available_width = constant_width + total_available_width * width_expand_ratio + size_hint and min values of None are resolved to 0.0. + """ - if child is not None and constant_width != 0 and constant_height != 0: - new_rect = child.rect - sh_w, sh_h = 0, 0 + min: float + max: float | None + hint: float + final_size: float = 0.0 + """The final size of the entry which will be returned by the algorithm""" + + @staticmethod + def from_widget(widget: UIWidget, dimension: Literal["width", "height"]) -> _C: + index = 0 if dimension == "width" else 1 + + # get hint values from different formats None and (float|None, float|None) + sh = widget.size_hint[index] if widget.size_hint else None + sh_min = widget.size_hint_min[index] if widget.size_hint_min else None + sh_max = widget.size_hint_max[index] if widget.size_hint_max else None + + # resolve min and max values if no size hint is given + min_value = widget.size[index] if sh is None else sh_min + max_value = widget.size[index] if sh is None else sh_max + + # clean up None values + min_value = min_value or 0 + sh = sh or 0.0 + + return _C( + min=min_value, + max=max_value, + hint=sh, + ) - if child.size_hint: - sh_w, sh_h = (child.size_hint[0] or 0), (child.size_hint[1] or 0) - shmn_w, shmn_h = child.size_hint_min or (None, None) - shmx_w, shmx_h = child.size_hint_max or (None, None) + @staticmethod + def from_widget_width(widget: UIWidget) -> _C: + return _C.from_widget(widget, "width") - new_height = max(shmn_h or 0, sh_h * available_height or child.height) - if shmx_h: - new_height = min(shmx_h, new_height) + @staticmethod + def from_widget_height(widget: UIWidget) -> _C: + return _C.from_widget(widget, "height") - new_width = max(shmn_w or 0, sh_w * available_width or child.width) - if shmx_w: - new_width = min(shmx_w, new_width) - new_rect = new_rect.resize(width=new_width, height=new_height) +def _box_orthogonal_algorithm(constraints: list[_C], container_size: float) -> List[float]: + """Calculate the 1 dimensional size of each entry based on the hint value and the available + space in the container. - cell_height = constant_height + self._vertical_spacing - cell_width = constant_width + self._horizontal_spacing + This calculation is done for the orthogonal axis of the box layout, which only applies the size + hint to the orthogonal axis. - center_y = start_y - (cell_height / 2) - center_x = start_x + (cell_width / 2) + Args: + constraints: List of constraints with hint, min and max values + container_size: The total size of the container + """ + # calculate the width of each entry based on the hint + for c in constraints: + size = container_size * c.hint + c.max = container_size if c.max is None else c.max - start_x += cell_width + c.final_size = min(max(c.min, size), c.max) # clamp width to min and max values - if self.align_vertical == "top": - new_rect = new_rect.align_top(start_y) - elif self.align_vertical == "bottom": - new_rect = new_rect.align_bottom(start_y - cell_height) - else: - new_rect = new_rect.align_y(center_y) + return [c.final_size for c in constraints] - if self.align_horizontal == "left": - new_rect = new_rect.align_left(start_x - cell_width) - elif self.align_horizontal == "right": - new_rect = new_rect.align_right(start_x) - else: - new_rect = new_rect.align_x(center_x) - child.rect = new_rect +def _box_axis_algorithm(constraints: list[_C], container_size: float) -> List[float]: + """ + The box algorithm calculates the 1 dimensional size of each entry based on the hint value and + the available space in the container. - # done due to row-wise rendering as start_y doesn't resets - # like start_x, specific to row span. - row_span = max_height_per_row[row_num][col_num][1] or 1 - actual_row_height = cell_height / row_span - if actual_row_height > max_height_row: - max_height_row = actual_row_height + Args: + constraints: List of constraints with hint, min and max values + container_size: The total size of the container - start_y -= max_height_row + Returns: + List of tuples with the sizes of each element + """ + # normalize hint - which will cover cases, where the sum of the hints is greater than 1. + # children will get a relative size based on their hint + total_hint = sum(c.hint for c in constraints) + if total_hint > 1: + for c in constraints: + c.hint /= total_hint + + # calculate the width of each entry based on the hint + for c in constraints: + size = container_size * c.hint + c.min = c.min or 0 + max_value = container_size if c.max is None else c.max + + c.final_size = min(max(c.min, size), max_value) # clamp width to min and max values + + # ---- Constantin + # calculate scaling factor + total_size = sum(c.final_size for c in constraints) + growth_diff = total_size - sum(c.min for c in constraints) # calculate available space + total_adjustable_size = container_size - sum(c.min for c in constraints) + + if growth_diff != 0: + scaling_factor = total_adjustable_size / growth_diff + + # adjust final_width based on scaling factor if scaling factor is less than 1 + if scaling_factor < 1: + for c in constraints: + c.final_size *= scaling_factor + + # recheck min constraints + for c in constraints: + c.final_size = max(c.final_size, c.min) + + # recheck max constraints + for c in constraints: + max_value = container_size if c.max is None else c.max + c.final_size = min(c.final_size, max_value) + + return [c.final_size for c in constraints] diff --git a/arcade/gui/widgets/slider.py b/arcade/gui/widgets/slider.py index 5569d624eb..acca619781 100644 --- a/arcade/gui/widgets/slider.py +++ b/arcade/gui/widgets/slider.py @@ -8,7 +8,7 @@ from typing_extensions import override import arcade -from arcade import Texture +from arcade import Texture, uicolor from arcade.gui import ( NinePatchTexture, Surface, @@ -20,7 +20,7 @@ from arcade.gui.events import UIOnChangeEvent from arcade.gui.property import Property, bind from arcade.gui.style import UIStyleBase, UIStyledWidget -from arcade.types import RGBA255, Color +from arcade.types import RGBA255 class UIBaseSlider(UIInteractiveWidget, metaclass=ABCMeta): @@ -92,13 +92,13 @@ def __init__( bind(self, "pressed", self.trigger_render) bind(self, "disabled", self.trigger_render) - self.register_event_type("on_change") # type: ignore # https://github.com/pyglet/pyglet/pull/1173 # noqa + self.register_event_type("on_change") def _x_for_value(self, value: float): """Provides the x coordinate for the given value.""" x = self.content_rect.left - val = (value - self.min_value) / self.max_value + val = (value - self.min_value) / (self.max_value - self.min_value) return x + self._cursor_width + val * (self.content_width - 2 * self._cursor_width) @property @@ -223,11 +223,11 @@ class UISliderStyle(UIStyleBase): """ - bg: RGBA255 = Color(94, 104, 117) - border: RGBA255 = Color(77, 81, 87) - border_width: int = 1 - filled_track: RGBA255 = Color(50, 50, 50) - unfilled_track: RGBA255 = Color(116, 125, 123) + bg: RGBA255 = uicolor.WHITE_SILVER + border: RGBA255 = uicolor.DARK_BLUE_MIDNIGHT_BLUE + border_width: int = 2 + filled_track: RGBA255 = uicolor.DARK_BLUE_MIDNIGHT_BLUE + unfilled_track: RGBA255 = uicolor.WHITE_SILVER class UISlider(UIStyledWidget[UISliderStyle], UIBaseSlider): @@ -257,25 +257,21 @@ class UISlider(UIStyledWidget[UISliderStyle], UIBaseSlider): DEFAULT_STYLE = { "normal": UIStyle(), "hover": UIStyle( - bg=Color(96, 103, 112), - border=Color(77, 81, 87), + border=uicolor.BLUE_PETER_RIVER, border_width=2, - filled_track=Color(50, 50, 50), - unfilled_track=Color(116, 125, 123), + filled_track=uicolor.BLUE_PETER_RIVER, ), "press": UIStyle( - bg=Color(96, 103, 112), - border=Color(77, 81, 87), + bg=uicolor.BLUE_PETER_RIVER, + border=uicolor.DARK_BLUE_WET_ASPHALT, border_width=3, - filled_track=Color(50, 50, 50), - unfilled_track=Color(116, 125, 123), + filled_track=uicolor.BLUE_PETER_RIVER, ), "disabled": UIStyle( - bg=Color(94, 104, 117), - border=Color(77, 81, 87), + bg=uicolor.WHITE_SILVER, border_width=1, - filled_track=Color(50, 50, 50), - unfilled_track=Color(116, 125, 123), + filled_track=uicolor.GRAY_ASBESTOS, + unfilled_track=uicolor.WHITE_SILVER, ), } @@ -288,7 +284,7 @@ def __init__( x: float = 0, y: float = 0, width: float = 300, - height: float = 20, + height: float = 25, size_hint=None, size_hint_min=None, size_hint_max=None, @@ -333,7 +329,7 @@ def _render_track(self, surface: Surface): bg_slider_color = style.get("unfilled_track", UISlider.UIStyle.unfilled_track) fg_slider_color = style.get("filled_track", UISlider.UIStyle.filled_track) - slider_height = self.content_height // 4 + slider_height = self.content_height // 3 slider_left_x = self._x_for_value(self.min_value) slider_right_x = self._x_for_value(self.max_value) @@ -387,24 +383,30 @@ class UITextureSlider(UISlider): You can copy this as-is into your own project, or you can modify the class to have more features as needed. + + Args: + track_texture: Texture for the track, should be a NinePatchTexture. + thumb_texture: Texture for the thumb. + style: Used to style the slider for different states. + **kwargs: Passed to UISlider. """ def __init__( self, - track: Union[Texture, NinePatchTexture], - thumb: Union[Texture, NinePatchTexture], + track_texture: Union[Texture, NinePatchTexture], + thumb_texture: Union[Texture, NinePatchTexture], style=None, **kwargs, ): - self._track = track - self._thumb = thumb + self._track_tex = track_texture + self._thumb_tex = thumb_texture super().__init__(style=style or UISlider.DEFAULT_STYLE, **kwargs) @override def _render_track(self, surface: Surface): style: UISliderStyle = self.get_current_style() # type: ignore - surface.draw_texture(0, 0, self.width, self.height, self._track) + surface.draw_texture(0, 0, self.width, self.height, self._track_tex) # TODO accept these as constructor params slider_height = self.height // 4 @@ -427,9 +429,9 @@ def _render_thumb(self, surface: Surface): cursor_center_x = self._thumb_x rel_cursor_x = cursor_center_x - self.left surface.draw_texture( - x=rel_cursor_x - self._thumb.width // 4 + 2, + x=rel_cursor_x - self._thumb_tex.width // 4 + 2, y=0, - width=self._thumb.width // 2, + width=self._thumb_tex.width // 2, height=self.height, - tex=self._thumb, + tex=self._thumb_tex, ) diff --git a/arcade/gui/widgets/text.py b/arcade/gui/widgets/text.py index 7be6a32ffe..62f40ad193 100644 --- a/arcade/gui/widgets/text.py +++ b/arcade/gui/widgets/text.py @@ -6,7 +6,7 @@ from pyglet.event import EVENT_HANDLED, EVENT_UNHANDLED from pyglet.text.caret import Caret from pyglet.text.document import AbstractDocument -from typing_extensions import override +from typing_extensions import Literal, override import arcade from arcade.gui.events import ( @@ -15,6 +15,7 @@ UIMouseEvent, UIMousePressEvent, UIMouseScrollEvent, + UIOnChangeEvent, UITextInputEvent, UITextMotionEvent, UITextMotionSelectEvent, @@ -83,7 +84,7 @@ def __init__( y: float = 0, width: Optional[float] = None, height: Optional[float] = None, - font_name=("Arial",), + font_name=("calibri", "arial"), font_size: float = 12, text_color: RGBOrA255 = arcade.color.WHITE, bold=False, @@ -221,6 +222,8 @@ def update_font( font_name: Optional[FontNameOrNames] = None, font_size: Optional[float] = None, font_color: Optional[Color] = None, + bold: Optional[bool | str] = None, + italic: Optional[bool] = None, ): """Update font of the label. @@ -230,20 +233,34 @@ def update_font( success. font_size: Font size of font. font_color: Color of the text. + bold: If enabled, the label's text will be in a **bold** style. + italic: If enabled, the label's text will be in an *italic* """ font_name = font_name or self._label.font_name font_size = font_size or self._label.font_size font_color = font_color or self._label.color + font_bold = bold if bold is not None else self._label.bold + font_italic = italic if italic is not None else self._label.italic # Check if values actually changed, if then update and trigger render font_name_changed = self._label.font_name != font_name font_size_changed = self._label.font_size != font_size font_color_changed = self._label.color != font_color - if font_name_changed or font_size_changed or font_color_changed: + font_bold_changed = self._label.bold != font_bold + font_italic_changed = self._label.italic != font_italic + if ( + font_name_changed + or font_size_changed + or font_color_changed + or font_bold_changed + or font_italic_changed + ): with self._label: self._label.font_name = font_name self._label.font_size = font_size self._label.color = font_color + self._label.bold = font_bold + self._label.italic = font_italic self._update_size_hint_min() # Optimised render behaviour @@ -288,6 +305,7 @@ class UITextWidget(UIAnchorLayout): def __init__(self, *, text: str, multiline: bool = False, **kwargs): super().__init__(text=text, **kwargs) + self._restrict_child_size = True self._label = UILabel( text=text, multiline=multiline ) # UILabel supports width=None for multiline @@ -364,6 +382,8 @@ class UIInputText(UIWidget): By default, a border is drawn around the input field. + The widget emits a :py:class:`~arcade.gui.UIOnChangeEvent` event when the text changes. + Args: x: x position (default anchor is bottom-left). y: y position (default anchor is bottom-left). @@ -454,6 +474,8 @@ def __init__( self._blink_state = self._get_caret_blink_state() + self.register_event_type("on_change") + def _get_caret_blink_state(self): """Check whether or not the caret is currently blinking or not.""" return self.caret.visible and self._active and self.caret._blink_visible @@ -492,6 +514,7 @@ def on_event(self, event: UIEvent) -> Optional[bool]: # If active pass all non press events to caret if self._active: + old_text = self.text # Act on events if active if isinstance(event, UITextInputEvent): self.caret.on_text(event.text) @@ -515,6 +538,9 @@ def on_event(self, event: UIEvent) -> Optional[bool]: self.caret.on_mouse_scroll(x, y, event.scroll_x, event.scroll_y) self.trigger_full_render() + if old_text != self.text: + self.dispatch_event("on_change", UIOnChangeEvent(self, old_text, self.text)) + if super().on_event(event): return EVENT_HANDLED @@ -563,7 +589,9 @@ def text(self): @text.setter def text(self, value): if value != self.doc.text: + old_text = self.doc.text self.doc.text = value + self.dispatch_event("on_change", UIOnChangeEvent(self, old_text, self.text)) # if bg color or texture is set, render this widget only if self._bg_color or self._bg_tex: @@ -579,6 +607,10 @@ def do_render(self, surface: Surface): self.layout.draw() + def on_change(self, event: UIOnChangeEvent): + """Event handler for text change.""" + pass + class UITextArea(UIWidget): """A text area that allows users to view large documents of text by scrolling @@ -601,6 +633,11 @@ class UITextArea(UIWidget): of space of the parent should be requested. size_hint_min: Minimum size hint width and height in pixel. size_hint_max: Maximum size hint width and height in pixel. + document_mode: Mode of the document. Can be "PLAIN", "ATTRIBUTED", or "HTML". + PLAIN will decode the text as plain text, ATTRIBUTED and HTML will + decode the text as pyglet documents here + https://pyglet.readthedocs.io/en/latest/programming_guide/text.html + **kwargs: passed to :py:class:`~arcade.gui.UIWidget`. """ def __init__( @@ -611,7 +648,7 @@ def __init__( width: float = 400, height: float = 40, text: str = "", - font_name=("Arial",), + font_name=("arial", "calibri"), font_size: float = 12, text_color: RGBA255 = arcade.color.WHITE, multiline: bool = True, @@ -619,6 +656,7 @@ def __init__( size_hint=None, size_hint_min=None, size_hint_max=None, + document_mode: Literal["PLAIN", "ATTRIBUTED", "HTML"] = "PLAIN", **kwargs, ): super().__init__( @@ -636,10 +674,17 @@ def __init__( # Measured in pixels per 'click' self.scroll_speed = scroll_speed if scroll_speed is not None else font_size - self.doc: AbstractDocument = pyglet.text.decode_text(text) + self.doc: AbstractDocument + if document_mode == "PLAIN": + self.doc = pyglet.text.decode_text(text) + elif document_mode == "ATTRIBUTED": + self.doc = pyglet.text.decode_attributed(text) + elif document_mode == "HTML": + self.doc = pyglet.text.decode_html(text) + self.doc.set_style( 0, - 12, + len(text), dict( font_name=font_name, font_size=font_size, diff --git a/arcade/gui/widgets/toggle.py b/arcade/gui/widgets/toggle.py index 1d407a452a..6f9e2463ef 100644 --- a/arcade/gui/widgets/toggle.py +++ b/arcade/gui/widgets/toggle.py @@ -84,7 +84,7 @@ def __init__( ) self.value = value - self.register_event_type("on_change") # type: ignore # https://github.com/pyglet/pyglet/pull/1173 # noqa + self.register_event_type("on_change") bind(self, "value", self.trigger_render) bind(self, "value", self._dispatch_on_change_event) diff --git a/arcade/resources/__init__.py b/arcade/resources/__init__.py index 2597b49bf0..0e90a26fd7 100644 --- a/arcade/resources/__init__.py +++ b/arcade/resources/__init__.py @@ -214,19 +214,19 @@ def list_built_in_assets( def load_system_fonts() -> None: """Loads all the fonts in arcade's system directory. - Currently this is only the Kenney fonts:: - - Kenney_Blocks.ttf - Kenney_Future.ttf - Kenney_Future_Narrow.ttf - Kenney_High.ttf - Kenney_High_Square.ttf - Kenney_Mini.ttf - Kenney_Mini_Square.ttf - Kenney_Pixel.ttf - Kenney_Pixel_Square.ttf - Kenney_Rocket.ttf - Kenney_Rocket_Square.ttf + Currently, this is only the Kenney fonts:: + + Kenney_Blocks.ttf - Kenney Blocks + Kenney_Future.ttf - Kenney Future + Kenney_Future_Narrow.ttf - Kenney Future Narrow + Kenney_High.ttf - Kenney High + Kenney_High_Square.ttf - Kenney High Square + Kenney_Mini.ttf - Kenney Mini + Kenney_Mini_Square.ttf - Kenney Mini Square + Kenney_Pixel.ttf - Kenney Pixel + Kenney_Pixel_Square.ttf - Kenney Pixel Square + Kenney_Rocket.ttf - Kenney Rocket + Kenney_Rocket_Square.ttf - Kenney Rocket Square """ from arcade.text import load_font diff --git a/arcade/resources/system/gui_basic_assets/button/red_disabled.png b/arcade/resources/system/gui_basic_assets/button/red_disabled.png new file mode 100644 index 0000000000..dbe90854eb Binary files /dev/null and b/arcade/resources/system/gui_basic_assets/button/red_disabled.png differ diff --git a/arcade/resources/system/gui_basic_assets/red_button_hover.png b/arcade/resources/system/gui_basic_assets/button/red_hover.png similarity index 100% rename from arcade/resources/system/gui_basic_assets/red_button_hover.png rename to arcade/resources/system/gui_basic_assets/button/red_hover.png diff --git a/arcade/resources/system/gui_basic_assets/button/red_normal.png b/arcade/resources/system/gui_basic_assets/button/red_normal.png new file mode 100644 index 0000000000..bbe73c0c15 Binary files /dev/null and b/arcade/resources/system/gui_basic_assets/button/red_normal.png differ diff --git a/arcade/resources/system/gui_basic_assets/red_button_press.png b/arcade/resources/system/gui_basic_assets/button/red_press.png similarity index 100% rename from arcade/resources/system/gui_basic_assets/red_button_press.png rename to arcade/resources/system/gui_basic_assets/button/red_press.png diff --git a/arcade/resources/system/gui_basic_assets/button_square_blue.png b/arcade/resources/system/gui_basic_assets/button_square_blue.png deleted file mode 100644 index c165e51f7f..0000000000 Binary files a/arcade/resources/system/gui_basic_assets/button_square_blue.png and /dev/null differ diff --git a/arcade/resources/system/gui_basic_assets/button_square_blue_pressed.png b/arcade/resources/system/gui_basic_assets/button_square_blue_pressed.png deleted file mode 100644 index 29d01f9cfa..0000000000 Binary files a/arcade/resources/system/gui_basic_assets/button_square_blue_pressed.png and /dev/null differ diff --git a/arcade/resources/system/gui_basic_assets/checkbox/blue_check.png b/arcade/resources/system/gui_basic_assets/checkbox/blue_check.png new file mode 100755 index 0000000000..f8bf9148fd Binary files /dev/null and b/arcade/resources/system/gui_basic_assets/checkbox/blue_check.png differ diff --git a/arcade/resources/system/gui_basic_assets/checkbox/blue_cross.png b/arcade/resources/system/gui_basic_assets/checkbox/blue_cross.png new file mode 100755 index 0000000000..efb0950077 Binary files /dev/null and b/arcade/resources/system/gui_basic_assets/checkbox/blue_cross.png differ diff --git a/arcade/resources/system/gui_basic_assets/checkbox/empty.png b/arcade/resources/system/gui_basic_assets/checkbox/empty.png new file mode 100755 index 0000000000..da487e36fc Binary files /dev/null and b/arcade/resources/system/gui_basic_assets/checkbox/empty.png differ diff --git a/arcade/resources/system/gui_basic_assets/checkbox/grey_check.png b/arcade/resources/system/gui_basic_assets/checkbox/grey_check.png new file mode 100755 index 0000000000..537b6869d8 Binary files /dev/null and b/arcade/resources/system/gui_basic_assets/checkbox/grey_check.png differ diff --git a/arcade/resources/system/gui_basic_assets/checkbox/grey_cross.png b/arcade/resources/system/gui_basic_assets/checkbox/grey_cross.png new file mode 100755 index 0000000000..1915105f53 Binary files /dev/null and b/arcade/resources/system/gui_basic_assets/checkbox/grey_cross.png differ diff --git a/arcade/resources/system/gui_basic_assets/checkbox/red_check.png b/arcade/resources/system/gui_basic_assets/checkbox/red_check.png new file mode 100755 index 0000000000..7fc4a72504 Binary files /dev/null and b/arcade/resources/system/gui_basic_assets/checkbox/red_check.png differ diff --git a/arcade/resources/system/gui_basic_assets/checkbox/red_cross.png b/arcade/resources/system/gui_basic_assets/checkbox/red_cross.png new file mode 100755 index 0000000000..bd72db5016 Binary files /dev/null and b/arcade/resources/system/gui_basic_assets/checkbox/red_cross.png differ diff --git a/arcade/resources/system/gui_basic_assets/items/shield_gold.png b/arcade/resources/system/gui_basic_assets/items/shield_gold.png deleted file mode 100644 index a4337a1506..0000000000 Binary files a/arcade/resources/system/gui_basic_assets/items/shield_gold.png and /dev/null differ diff --git a/arcade/resources/system/gui_basic_assets/items/sword_gold.png b/arcade/resources/system/gui_basic_assets/items/sword_gold.png deleted file mode 100644 index abe914fa24..0000000000 Binary files a/arcade/resources/system/gui_basic_assets/items/sword_gold.png and /dev/null differ diff --git a/arcade/resources/system/gui_basic_assets/red_button_normal.png b/arcade/resources/system/gui_basic_assets/red_button_normal.png deleted file mode 100644 index eb660d0ebd..0000000000 Binary files a/arcade/resources/system/gui_basic_assets/red_button_normal.png and /dev/null differ diff --git a/arcade/resources/system/gui_basic_assets/toggle/circle_switch_off.png b/arcade/resources/system/gui_basic_assets/simple_checkbox/circle_off.png similarity index 100% rename from arcade/resources/system/gui_basic_assets/toggle/circle_switch_off.png rename to arcade/resources/system/gui_basic_assets/simple_checkbox/circle_off.png diff --git a/arcade/resources/system/gui_basic_assets/toggle/circle_switch_on.png b/arcade/resources/system/gui_basic_assets/simple_checkbox/circle_on.png similarity index 100% rename from arcade/resources/system/gui_basic_assets/toggle/circle_switch_on.png rename to arcade/resources/system/gui_basic_assets/simple_checkbox/circle_on.png diff --git a/arcade/resources/system/gui_basic_assets/toggle/switch_green.png b/arcade/resources/system/gui_basic_assets/simple_checkbox/switch_green.png similarity index 100% rename from arcade/resources/system/gui_basic_assets/toggle/switch_green.png rename to arcade/resources/system/gui_basic_assets/simple_checkbox/switch_green.png diff --git a/arcade/resources/system/gui_basic_assets/toggle/switch_red.png b/arcade/resources/system/gui_basic_assets/simple_checkbox/switch_red.png similarity index 100% rename from arcade/resources/system/gui_basic_assets/toggle/switch_red.png rename to arcade/resources/system/gui_basic_assets/simple_checkbox/switch_red.png diff --git a/arcade/resources/system/gui_basic_assets/slider/thumb_blue.png b/arcade/resources/system/gui_basic_assets/slider/thumb_blue.png new file mode 100644 index 0000000000..ba977afe55 Binary files /dev/null and b/arcade/resources/system/gui_basic_assets/slider/thumb_blue.png differ diff --git a/arcade/resources/system/gui_basic_assets/slider/thumb_green.png b/arcade/resources/system/gui_basic_assets/slider/thumb_green.png new file mode 100644 index 0000000000..e77ad9bab9 Binary files /dev/null and b/arcade/resources/system/gui_basic_assets/slider/thumb_green.png differ diff --git a/arcade/resources/system/gui_basic_assets/slider/thumb_red.png b/arcade/resources/system/gui_basic_assets/slider/thumb_red.png new file mode 100644 index 0000000000..cf2dc4fa65 Binary files /dev/null and b/arcade/resources/system/gui_basic_assets/slider/thumb_red.png differ diff --git a/arcade/resources/system/gui_basic_assets/slider/track_blue.png b/arcade/resources/system/gui_basic_assets/slider/track_blue.png new file mode 100644 index 0000000000..79fb73e88b Binary files /dev/null and b/arcade/resources/system/gui_basic_assets/slider/track_blue.png differ diff --git a/arcade/resources/system/gui_basic_assets/slider/track_green.png b/arcade/resources/system/gui_basic_assets/slider/track_green.png new file mode 100644 index 0000000000..2cbf774d5b Binary files /dev/null and b/arcade/resources/system/gui_basic_assets/slider/track_green.png differ diff --git a/arcade/resources/system/gui_basic_assets/slider/track_red.png b/arcade/resources/system/gui_basic_assets/slider/track_red.png new file mode 100644 index 0000000000..b6a9d3efe8 Binary files /dev/null and b/arcade/resources/system/gui_basic_assets/slider/track_red.png differ diff --git a/arcade/resources/system/gui_basic_assets/slider_thumb.png b/arcade/resources/system/gui_basic_assets/slider_thumb.png deleted file mode 100644 index 7b833c375d..0000000000 Binary files a/arcade/resources/system/gui_basic_assets/slider_thumb.png and /dev/null differ diff --git a/arcade/resources/system/gui_basic_assets/slider_track.png b/arcade/resources/system/gui_basic_assets/slider_track.png deleted file mode 100644 index 433dfa3ed5..0000000000 Binary files a/arcade/resources/system/gui_basic_assets/slider_track.png and /dev/null differ diff --git a/arcade/resources/system/gui_basic_assets/toggle/green.png b/arcade/resources/system/gui_basic_assets/toggle/green.png new file mode 100755 index 0000000000..5949a11af3 Binary files /dev/null and b/arcade/resources/system/gui_basic_assets/toggle/green.png differ diff --git a/arcade/resources/system/gui_basic_assets/toggle/green_gray.png b/arcade/resources/system/gui_basic_assets/toggle/green_gray.png new file mode 100755 index 0000000000..ce9a9d2ae7 Binary files /dev/null and b/arcade/resources/system/gui_basic_assets/toggle/green_gray.png differ diff --git a/arcade/resources/system/gui_basic_assets/toggle/red.png b/arcade/resources/system/gui_basic_assets/toggle/red.png new file mode 100755 index 0000000000..ffab2b0b79 Binary files /dev/null and b/arcade/resources/system/gui_basic_assets/toggle/red.png differ diff --git a/arcade/resources/system/gui_basic_assets/toggle/red_gray.png b/arcade/resources/system/gui_basic_assets/toggle/red_gray.png new file mode 100755 index 0000000000..24537a2b93 Binary files /dev/null and b/arcade/resources/system/gui_basic_assets/toggle/red_gray.png differ diff --git a/arcade/resources/system/gui_basic_assets/window/panel_blue.png b/arcade/resources/system/gui_basic_assets/window/panel_blue.png new file mode 100644 index 0000000000..89681ffb2d Binary files /dev/null and b/arcade/resources/system/gui_basic_assets/window/panel_blue.png differ diff --git a/arcade/resources/system/gui_basic_assets/window/panel_gray.png b/arcade/resources/system/gui_basic_assets/window/panel_gray.png new file mode 100644 index 0000000000..62399fa516 Binary files /dev/null and b/arcade/resources/system/gui_basic_assets/window/panel_gray.png differ diff --git a/arcade/resources/system/gui_basic_assets/window/panel_green.png b/arcade/resources/system/gui_basic_assets/window/panel_green.png new file mode 100644 index 0000000000..8e8df15825 Binary files /dev/null and b/arcade/resources/system/gui_basic_assets/window/panel_green.png differ diff --git a/arcade/resources/system/gui_basic_assets/window/panel_red.png b/arcade/resources/system/gui_basic_assets/window/panel_red.png new file mode 100644 index 0000000000..1ddd77e476 Binary files /dev/null and b/arcade/resources/system/gui_basic_assets/window/panel_red.png differ 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/arcade/sprite/base.py b/arcade/sprite/base.py index 7cdeef6399..73ac6eb1b0 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -198,6 +198,7 @@ def size(self, new_value: Point2): if width != self._width or height != self._height: texture_width, texture_height = self._texture.size self._scale = width / texture_width, height / texture_height + self._hit_box.scale = self._scale self._width = width self._height = height diff --git a/arcade/text.py b/arcade/text.py index 1407d58a3d..6e74ee9061 100644 --- a/arcade/text.py +++ b/arcade/text.py @@ -537,7 +537,7 @@ def italic(self) -> bool | str: return self._label.italic @italic.setter - def italic(self, italic: bool): + def italic(self, italic: bool | str): self._label.italic = italic @property @@ -613,6 +613,39 @@ def position(self, point: Point): else: self._label.position = x, y, self._label.z + @property + def tracking(self) -> float | None: + """ + Get/set the tracking amount for this text object, or rather, + the added space between each character. + + The value is an amount in pixels and can be negative. + To convert from the em unit, use Text.em_to_px(). + + Returns: + a pixel amount, or None if the tracking is inconsistent. + """ + kerning = self._label.get_style("kerning") + return kerning if kerning != pyglet.text.document.STYLE_INDETERMINATE else None + + @tracking.setter + def tracking(self, value: float): + self._label.set_style("kerning", value) + + def em_to_px(self, em: float) -> float: + """Convert from an em value to a pixel amount. + + 1em is defined as ``font_size`` pt. + """ + return (em * self.font_size) * (4 / 3) + + def px_to_em(self, px: float) -> float: + """Convert from a pixel amount to a value in ems. + + 1em is defined as ``font_size`` pt. + """ + return px / (4 / 3) / self.font_size + def create_text_sprite( text: str, diff --git a/arcade/uicolor.py b/arcade/uicolor.py new file mode 100644 index 0000000000..e2cd86c619 --- /dev/null +++ b/arcade/uicolor.py @@ -0,0 +1,50 @@ +""" +This module pre-defines colors from FlatUI. +https://materialui.co/flatuicolors/ +""" + +from arcade.color import Color + +GREEN_TURQUOISE = Color(26, 188, 156) +GREEN_GREEN_SEA = Color(22, 160, 133) +GREEN_NEPHRITIS = Color(39, 174, 96) +GREEN_EMERALD = Color(46, 204, 113) +BLUE_PETER_RIVER = Color(52, 152, 219) +BLUE_BELIZE_HOLE = Color(41, 128, 185) +PURPLE_AMETHYST = Color(155, 89, 182) +PURPLE_WISTERIA = Color(142, 68, 173) +DARK_BLUE_WET_ASPHALT = Color(52, 73, 94) +DARK_BLUE_MIDNIGHT_BLUE = Color(44, 62, 80) +YELLOW_SUN_FLOWER = Color(241, 196, 15) +YELLOW_ORANGE = Color(243, 156, 18) +ORANGE_CARROT = Color(230, 126, 34) +ORANGE_PUMPKIN = Color(211, 84, 0) +RED_ALIZARIN = Color(231, 76, 60) +RED_POMEGRANATE = Color(192, 57, 43) +WHITE_CLOUDS = Color(236, 240, 241) +WHITE_SILVER = Color(189, 195, 199) +GRAY_CONCRETE = Color(149, 165, 166) +GRAY_ASBESTOS = Color(127, 140, 141) + +__all__ = [ + "GREEN_TURQUOISE", + "GREEN_GREEN_SEA", + "GREEN_NEPHRITIS", + "GREEN_EMERALD", + "BLUE_PETER_RIVER", + "BLUE_BELIZE_HOLE", + "PURPLE_AMETHYST", + "PURPLE_WISTERIA", + "DARK_BLUE_WET_ASPHALT", + "DARK_BLUE_MIDNIGHT_BLUE", + "YELLOW_SUN_FLOWER", + "YELLOW_ORANGE", + "ORANGE_CARROT", + "ORANGE_PUMPKIN", + "RED_ALIZARIN", + "RED_POMEGRANATE", + "WHITE_CLOUDS", + "WHITE_SILVER", + "GRAY_CONCRETE", + "GRAY_ASBESTOS", +] diff --git a/doc/example_code/gui_0_basic_setup.rst b/doc/example_code/gui_0_basic_setup.rst new file mode 100644 index 0000000000..ba3f81fa6c --- /dev/null +++ b/doc/example_code/gui_0_basic_setup.rst @@ -0,0 +1,15 @@ +:orphan: + +.. _gui_0_basic_setup: + +GUI Basic Setup +=============== + +.. image:: images/gui_0_basic_setup.png + :width: 600px + :align: center + :alt: Screen shot of GUI Basic Setup + +.. literalinclude:: ../../arcade/examples/gui/0_basic_setup.py + :caption: 0_basic_setup.py + :linenos: diff --git a/doc/example_code/gui_1_layouts.rst b/doc/example_code/gui_1_layouts.rst new file mode 100644 index 0000000000..93439862c8 --- /dev/null +++ b/doc/example_code/gui_1_layouts.rst @@ -0,0 +1,15 @@ +:orphan: + +.. _gui_1_layouts: + +GUI Layouts +=========== + +.. image:: images/gui_1_layouts.png + :width: 600px + :align: center + :alt: Screen shot of GUI Basic Setup + +.. literalinclude:: ../../arcade/examples/gui/1_layouts.py + :caption: 1_layouts.py + :linenos: diff --git a/doc/example_code/gui_2_widgets.rst b/doc/example_code/gui_2_widgets.rst new file mode 100644 index 0000000000..955e645dc8 --- /dev/null +++ b/doc/example_code/gui_2_widgets.rst @@ -0,0 +1,15 @@ +:orphan: + +.. _gui_2_widgets: + +GUI Widget Gallery +================== + +.. image:: images/gui_2_widgets.png + :width: 600px + :align: center + :alt: Screen shot of GUI Widget Gallery + +.. literalinclude:: ../../arcade/examples/gui/2_widgets.py + :caption: 2_widgets.py + :linenos: diff --git a/doc/example_code/gui_3_buttons.rst b/doc/example_code/gui_3_buttons.rst new file mode 100644 index 0000000000..21442dbf4c --- /dev/null +++ b/doc/example_code/gui_3_buttons.rst @@ -0,0 +1,15 @@ +:orphan: + +.. _gui_3_buttons: + +GUI Advanced Buttons +==================== + +.. image:: images/gui_3_buttons.png + :width: 600px + :align: center + :alt: Screen shot of advanced button usage + +.. literalinclude:: ../../arcade/examples/gui/3_buttons.py + :caption: 3_buttons.py + :linenos: diff --git a/doc/example_code/gui_4_with_camera.rst b/doc/example_code/gui_4_with_camera.rst new file mode 100644 index 0000000000..286dcd11d5 --- /dev/null +++ b/doc/example_code/gui_4_with_camera.rst @@ -0,0 +1,15 @@ +:orphan: + +.. _gui_4_with_camera: + +GUI with Camera +=============== + +.. image:: images/gui_4_with_camera.png + :width: 600px + :align: center + :alt: Screen shot of advanced button usage + +.. literalinclude:: ../../arcade/examples/gui/4_with_camera.py + :caption: 4_with_camera.py + :linenos: diff --git a/doc/example_code/gui_5_uicolor_picker.rst b/doc/example_code/gui_5_uicolor_picker.rst new file mode 100644 index 0000000000..c334137b1e --- /dev/null +++ b/doc/example_code/gui_5_uicolor_picker.rst @@ -0,0 +1,21 @@ +:orphan: + +.. _gui_5_uicolor_picker: + +GUI UIColor Picker +================== + +Arcade provides constants for colors, which work great for GUI widgets. +They are based on FlatUI colors. + +This example provides an interactive color picker, widgets can be used as a util +to find the right color for your GUI widgets. + +.. image:: images/gui_5_uicolor_picker.png + :width: 600px + :align: center + :alt: Screen shot of advanced button usage + +.. literalinclude:: ../../arcade/examples/gui/5_uicolor_picker.py + :caption: 5_uicolor_picker.py + :linenos: diff --git a/doc/example_code/gui_exp_hidden_password.rst b/doc/example_code/gui_exp_hidden_password.rst new file mode 100644 index 0000000000..1851ff290a --- /dev/null +++ b/doc/example_code/gui_exp_hidden_password.rst @@ -0,0 +1,17 @@ +:orphan: + +.. _gui_exp_hidden_password: + +GUI Hidden Password +=================== + +The following example demonstrates how to make use of the experimental widget. + +.. image:: images/gui_exp_hidden_password.png + :width: 600px + :align: center + :alt: Screen shot + +.. literalinclude:: ../../arcade/examples/gui/exp_hidden_password.py + :caption: exp_hidden_password.py + :linenos: diff --git a/doc/example_code/gui_flat_button.rst b/doc/example_code/gui_flat_button.rst deleted file mode 100644 index b3e6300318..0000000000 --- a/doc/example_code/gui_flat_button.rst +++ /dev/null @@ -1,60 +0,0 @@ -:orphan: - -.. _gui_flat_button: - -Flat Text Buttons -================= - -For an introduction the GUI system, see :ref:`gui_concepts`. - -The :class:`arcade.gui.UIFlatButton` is a simple button with a text label. -It doesn't have any three-dimensional look to it. - -.. image:: images/gui_flat_button.png - :width: 600px - :align: center - :alt: Screen shot of flat text buttons - -There are three ways to process button click events: - -1. Create a class with a parent class of `arcade.UIFlatButton` - and implement a method called `on_click`. -2. Create a button, then set the `on_click` attribute of that button to - equal the function you want to be called. -3. Create a button. Then use a decorator to specify a method to call - when an `on_click` event occurs for that button. - -This code shows each of the three ways above. Code should pick ONE of the three -ways and standardize on it though-out the program. Do NOT write code -that uses all three ways. - - -.. literalinclude:: ../../arcade/examples/gui_flat_button.py - :caption: gui_flat_button.py - :linenos: - -See :class:`arcade.gui.UIBoxLayout` and :class:`arcade.gui.UIAnchorLayout` -for more information about positioning the buttons. -For example, this change to line 31: - -.. code-block:: python - - self.v_box = arcade.gui.widgets.layout.UIBoxLayout(space_between=20, vertical=False); - -and to line 60: - -.. code-block:: python - - ui_anchor_layout.add(child=self.v_box, - anchor_x="left", - anchor_y="bottom", - align_x=10, - align_y=10); - -in the code above will align the buttons horizontally and anchor them to the -bottom left of the window with 10px margins. - -.. image:: images/gui_flat_button_positioned.png - :width: 600px - :align: center - :alt: Screen shot of flat text buttons in bottom left of window \ No newline at end of file diff --git a/doc/example_code/gui_flat_button_styled.rst b/doc/example_code/gui_flat_button_styled.rst deleted file mode 100644 index c75cf4d185..0000000000 --- a/doc/example_code/gui_flat_button_styled.rst +++ /dev/null @@ -1,10 +0,0 @@ -:orphan: - -.. _gui_flat_button_styled: - -Flat Text Button Styled -======================= - -.. literalinclude:: ../../arcade/examples/gui_flat_button_styled.py - :caption: gui_flat_button_styled.py - :linenos: diff --git a/doc/example_code/gui_ok_messagebox.rst b/doc/example_code/gui_ok_messagebox.rst deleted file mode 100644 index 54a45d7984..0000000000 --- a/doc/example_code/gui_ok_messagebox.rst +++ /dev/null @@ -1,21 +0,0 @@ -:orphan: - -.. _gui_ok_messagebox: - -OK Message Box -============== - -For an introduction the GUI system, see :ref:`gui_concepts`. - -This example shows how to pop up a quick message box for the user to click 'Ok' on -using the :class:`arcade.gui.OKMessagebox` class. - -.. image:: images/gui_ok_messagebox.png - :width: 600px - :align: center - :alt: Screen shot OKMessageBox in action - -.. literalinclude:: ../../arcade/examples/gui_ok_messagebox.py - :caption: gui_ok_messagebox.py - :linenos: - :emphasize-lines: 37-47 diff --git a/doc/example_code/gui_scrollable_text.rst b/doc/example_code/gui_scrollable_text.rst deleted file mode 100644 index 10289a678e..0000000000 --- a/doc/example_code/gui_scrollable_text.rst +++ /dev/null @@ -1,17 +0,0 @@ -:orphan: - -.. _gui_scrollable_text: - -GUI Scrollable Text -=================== - -For an introduction the GUI system, see :ref:`gui_concepts`. - -.. image:: images/gui_scrollable_text.png - :width: 600px - :align: center - :alt: Screen shot gui_scrollable_text in action - -.. literalinclude:: ../../arcade/examples/gui_scrollable_text.py - :caption: gui_scrollable_text.py - :linenos: diff --git a/doc/example_code/gui_slider.rst b/doc/example_code/gui_slider.rst deleted file mode 100644 index 5a6458552e..0000000000 --- a/doc/example_code/gui_slider.rst +++ /dev/null @@ -1,15 +0,0 @@ -:orphan: - -.. _gui_slider: - -GUI Slider -========== - -.. image:: images/gui_slider.png - :width: 600px - :align: center - :alt: Screen shot of a GUI slider in the example - -.. literalinclude:: ../../arcade/examples/gui_slider.py - :caption: gui_slider.py - :linenos: diff --git a/doc/example_code/gui_widgets.rst b/doc/example_code/gui_widgets.rst deleted file mode 100644 index beceb9fa11..0000000000 --- a/doc/example_code/gui_widgets.rst +++ /dev/null @@ -1,15 +0,0 @@ -:orphan: - -.. _gui_widgets: - -GUI Widgets -=========== - -.. image:: images/gui_widgets.png - :width: 600px - :align: center - :alt: Screen shot GUI Widgets - -.. literalinclude:: ../../arcade/examples/gui_widgets.py - :caption: gui_widgets.py - :linenos: diff --git a/doc/example_code/images/gui_0_basic_setup.png b/doc/example_code/images/gui_0_basic_setup.png new file mode 100644 index 0000000000..ae871a253f Binary files /dev/null and b/doc/example_code/images/gui_0_basic_setup.png differ diff --git a/doc/example_code/images/gui_1_layouts.png b/doc/example_code/images/gui_1_layouts.png new file mode 100644 index 0000000000..962a63a195 Binary files /dev/null and b/doc/example_code/images/gui_1_layouts.png differ diff --git a/doc/example_code/images/gui_2_widgets.png b/doc/example_code/images/gui_2_widgets.png new file mode 100644 index 0000000000..18b25eefe9 Binary files /dev/null and b/doc/example_code/images/gui_2_widgets.png differ diff --git a/doc/example_code/images/gui_3_buttons.png b/doc/example_code/images/gui_3_buttons.png new file mode 100644 index 0000000000..75a50eeaff Binary files /dev/null and b/doc/example_code/images/gui_3_buttons.png differ diff --git a/doc/example_code/images/gui_4_with_camera.png b/doc/example_code/images/gui_4_with_camera.png new file mode 100644 index 0000000000..565cca456a Binary files /dev/null and b/doc/example_code/images/gui_4_with_camera.png differ diff --git a/doc/example_code/images/gui_5_uicolor_picker.png b/doc/example_code/images/gui_5_uicolor_picker.png new file mode 100644 index 0000000000..0b75f0f8f9 Binary files /dev/null and b/doc/example_code/images/gui_5_uicolor_picker.png differ diff --git a/doc/example_code/images/gui_exp_hidden_password.png b/doc/example_code/images/gui_exp_hidden_password.png new file mode 100644 index 0000000000..f35ea463fa Binary files /dev/null and b/doc/example_code/images/gui_exp_hidden_password.png differ diff --git a/doc/example_code/images/gui_flat_button.png b/doc/example_code/images/gui_flat_button.png deleted file mode 100644 index dfdfbec3f9..0000000000 Binary files a/doc/example_code/images/gui_flat_button.png and /dev/null differ diff --git a/doc/example_code/images/gui_flat_button_positioned.png b/doc/example_code/images/gui_flat_button_positioned.png deleted file mode 100644 index c77e9f3e10..0000000000 Binary files a/doc/example_code/images/gui_flat_button_positioned.png and /dev/null differ diff --git a/doc/example_code/images/gui_flat_button_styled.png b/doc/example_code/images/gui_flat_button_styled.png deleted file mode 100644 index 79c826751e..0000000000 Binary files a/doc/example_code/images/gui_flat_button_styled.png and /dev/null differ diff --git a/doc/example_code/images/gui_ok_messagebox.png b/doc/example_code/images/gui_ok_messagebox.png deleted file mode 100644 index 0cb64fa547..0000000000 Binary files a/doc/example_code/images/gui_ok_messagebox.png and /dev/null differ diff --git a/doc/example_code/images/gui_scrollable_text.png b/doc/example_code/images/gui_scrollable_text.png deleted file mode 100644 index e67aa90735..0000000000 Binary files a/doc/example_code/images/gui_scrollable_text.png and /dev/null differ diff --git a/doc/example_code/images/gui_slider.png b/doc/example_code/images/gui_slider.png deleted file mode 100644 index b40ed4399a..0000000000 Binary files a/doc/example_code/images/gui_slider.png and /dev/null differ diff --git a/doc/example_code/images/gui_widgets.png b/doc/example_code/images/gui_widgets.png deleted file mode 100644 index 31da2a7dd4..0000000000 Binary files a/doc/example_code/images/gui_widgets.png and /dev/null differ diff --git a/doc/example_code/index.rst b/doc/example_code/index.rst index efb60be791..6677459c7e 100644 --- a/doc/example_code/index.rst +++ b/doc/example_code/index.rst @@ -579,44 +579,71 @@ Procedural Generation :ref:`procedural_caves_bsp` + +.. _gui_examples_overview: + Graphical User Interface ------------------------ -.. figure:: images/thumbs/gui_flat_button.png + +.. figure:: images/thumbs/gui_0_basic_setup.png :figwidth: 170px - :target: gui_flat_button.html + :target: gui_0_basic_setup.html - :ref:`gui_flat_button` + :ref:`gui_0_basic_setup` -.. figure:: images/thumbs/gui_flat_button_styled.png +.. figure:: images/thumbs/gui_1_layouts.png :figwidth: 170px - :target: gui_flat_button_styled.html + :target: gui_1_layouts.html + + :ref:`gui_1_layouts` - :ref:`gui_flat_button_styled` +.. figure:: images/thumbs/gui_2_widgets.png + :figwidth: 170px + :target: gui_2_widgets.html -.. figure:: images/thumbs/gui_widgets.png + :ref:`gui_2_widgets` + +.. figure:: images/thumbs/gui_3_buttons.png :figwidth: 170px - :target: gui_widgets.html + :target: gui_3_buttons.html - :ref:`gui_widgets` + :ref:`gui_3_buttons` -.. figure:: images/thumbs/gui_ok_messagebox.png +.. figure:: images/thumbs/gui_4_with_camera.png :figwidth: 170px - :target: gui_ok_messagebox.html + :target: gui_4_with_camera.html - :ref:`gui_ok_messagebox` + :ref:`gui_4_with_camera` -.. figure:: images/thumbs/gui_scrollable_text.png +.. figure:: images/thumbs/gui_5_uicolor_picker.png :figwidth: 170px - :target: gui_scrollable_text.html + :target: gui_5_uicolor_picker.html + + :ref:`gui_5_uicolor_picker` + +.. note:: - :ref:`gui_scrollable_text` + Not all existing examples made it into this section. You can find more under `Arcade GUI Examples `_ -.. figure:: images/thumbs/gui_slider.png +Experimental Widgets +^^^^^^^^^^^^^^^^^^^^ + +.. figure:: images/thumbs/gui_exp_hidden_password.png :figwidth: 170px - :target: gui_slider.html + :target: gui_exp_hidden_password.html + + :ref:`gui_exp_hidden_password` + + +.. note:: + + Experimental widgets are not yet part of the official release. + They are subject to change and may not be fully functional. + + Feedback is very welcome, please let us know what you think about them. + - :ref:`gui_slider` Grid-Based Games ---------------- diff --git a/doc/index.rst b/doc/index.rst index c23d2030f7..de225261ea 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -101,6 +101,7 @@ Arcade is developed by volunteers. programming_guide/sound programming_guide/textures programming_guide/event_loop + programming_guide/camera programming_guide/sections programming_guide/gui/index programming_guide/texture_atlas @@ -108,7 +109,6 @@ Arcade is developed by volunteers. programming_guide/opengl_notes programming_guide/performance_tips programming_guide/headless - programming_guide/vsync .. toctree:: :hidden: diff --git a/doc/programming_guide/camera.rst b/doc/programming_guide/camera.rst new file mode 100644 index 0000000000..5a787fc845 --- /dev/null +++ b/doc/programming_guide/camera.rst @@ -0,0 +1,2 @@ +Camera +====== diff --git a/doc/programming_guide/event_loop.rst b/doc/programming_guide/event_loop.rst index c1fb2b6708..dc5ad456cf 100644 --- a/doc/programming_guide/event_loop.rst +++ b/doc/programming_guide/event_loop.rst @@ -4,35 +4,151 @@ Event Loop Introduction ------------ -Python Arcade provides a simple three event loop to build off. +Python Arcade provides three simple methods to integrate with the event loop. +All three methods are exposed to be overridden in :py:class:`arcade.Window` +and :py:class:`arcade.View`. For advanced use cases it is possible to add own +handler via :py:func:`arcade.Window.push_handlers`. :py:func:`on_draw` - is provided to render to the window. After the ``on_draw`` event the - window will present itself to the screen. By default this attempts - to occur every 1/60 seconds or around 16.7 milliseconds. This can be - changed when initialising your :py:class:`arcade.Window` with the - ``draw_rate`` argument. Setting the draw rate to a value above a - screens refresh rate can cause tearing unless the ``vsync`` argument - is set to true. It is recommended to keep your ``draw_rate`` around - the refresh rate of the screen as it does not prevent the other events - from occuring. +^^^^^^^^^^^^^^^^^^ +provides a hook to render to the window. After the ``on_draw`` event, the window +will draw to the screen. By default, this attempts to occur every 1/60 seconds +or once every 16.7 milliseconds. It can be changed when initializing your +:py:class:`arcade.Window` with the ``draw_rate`` argument. Setting the draw rate +to a value above a screen's refresh rate can cause tearing unless you set the +``vsync`` argument to true. We recommend keeping your ``draw_rate`` around the +screen's refresh rate. After every draw event camera state will be reset. +This means that non-default cameras must be reused on every draw event. :py:func:`on_update` - is provided to update state which needs to happen at a roughly regular interval. - The update event is not strictly paired to the draw event, but they share the same - thread. This can cause a bottle-neck if one is significantly slower than the other. - The event also provides a ``delta_time`` argument which is the time elapsed since the - last ``on_update`` event. +^^^^^^^^^^^^^^^^^^^^ +provides a hook to update state which needs to happen at a roughly regular interval. +The update event is not strictly paired to the draw event, but they share the same +thread. This can cause a bottle-neck if one is significantly slower than the other. +The event also provides a ``delta_time`` argument which is the time elapsed since the +last ``on_update`` event. You can change the rate at which ``on_update`` is called with +the ``update_rate`` argument when initialising your :py:class:`arcade.Window`. :py:func:`on_fixed_update` - is provided to update state which must happen with an exactly regular interval. +^^^^^^^^^^^^^^^^^^^^^^^^^^ +provides a hook to update state which must happen with an exactly regular interval. +Because Arcade can't ensure the event is actually fired regularly it stores how +much time has passed since the last update, and once enough time has passed it +releases an ``on_fixed_update`` call. The fixed update always provides the same +``delta_time`` argument. You can change the rate at which ``on__fixed_update`` is +called with the ``fixed_rate`` argument when initialising your :py:class:`arcade.Window`. -**TODO**: add note about camera state resetting once that's in +Time +---- +While the underlying library, pyglet, provide a clock for scheduling events it is closely tied +to the window's own events. For simple time keeping arcade provides global +clock objects. Both clocks can be imported from ``arcade.clock`` as +``GLOBAL_CLOCK`` and ``GLOBAL_FIXED_CLOCK`` -All three methods are exposed to be overridden in :py:class:`arcade.Window` -and :py:class:`arcade.View`. You may also register your own handlers -to these events using :py:func:`arcade.Window.push_handlers`, but this is -not recommended for beginners. +:py:class:`arcade.Clock` +^^^^^^^^^^^^^^^^^^^^^^^^ +The base arcade clock tracks the elapsed time in seconds, the total number +of clock ticks, and the amount of time that elapsed since the last tick. +The currently active window automatically ticks the ``GLOBAL_CLOCK`` every ``on_update``. +This means there is no reason to manually tick it. If you need more +clocks, possibly ticking at a different rate, an :py:class:`arcade.Clock` +can be created on the fly. -Time -==== \ No newline at end of file +:py:class:`arcade.FixedClock` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +The fixed clock tracks the same values as the normal clock, but has two special features. +Firstly it enforces that the ``delta_time`` passed into its ``tick`` method is always the same. +This is because advanced physics engines require consistent time. Secondly the fixed clock +requires a sibling regular clock. It uses this clock to track how offset from the true time it is. +Like the regular clock you may make a new :py:class:`arcade.FixedClock` at any time, +but ensure they have a sibling. + +Up Coming +^^^^^^^^^ +In future version of arcade :py:class:`Clock` will be updated to allow for sub clocks. +Sub clocks will be ticked by their parent clock rather than be manually updated. Sub clocks +will make it easier to control the flow of time for specific groups of objects. Such as only +slowing enemies or excluding UI elements. To gain access to a draft :py:class:`arcade.Clock` +you can find it in ``arcade.future.sub_clock``. This version of the sub clock is not final. +If you find any bugs do not hesitate to raise an issue on the github. + +More on Fixed update +-------------------- +The ``on_fixed_update`` event can be an extremely powerful tool, but it has many complications +that should be taken into account. If used imporperly the event can grind a game to a halt. + +Death Spiral +^^^^^^^^^^^^ +A fixed update represents a very specific amount of time. If all of the computations take +longer than the fixed update represents than the ammount of time accumulated between update +events will grow. If this happens for multiple frames the game will begin to spiral. The +first few frames of the spiral will lead to one update cycle requiring two fixed update +calls. This will increase the extra time accumulated until three fixed updates must occur at once. +This will continue to happen until either: the fixed updates start taking less time, or the game +crashes. + +There are a few solutions to this issue. The simplist method, which works best when there may be spikes in +computation time that quickly settle, is to clamp the max number of fixed updates that can occur in a single +frame. In arcade this is done by setting the ``fixed_frame_cap`` argument when initialising your +:py:class:`arcade.Window`. The second method is to slow-down time temporarily. By changing the +``_tick_speed`` of arcade's ``GLOBAL_CLOCK`` is is possible to slow down the accumulation of time. +For example setting ``GLOBAL_CLOCK._tick_speed = 0.5`` would allow the fixed update twice as many frames +to calculate for. + +Update Interpolation +^^^^^^^^^^^^^^^^^^^^ +Because fixed updates work on the accumulation of time this may not sync with +the ``on_draw`` or ``on_update`` events. In extreme cases this can cause a visible stuttering to +objects moved within ``on_fixed_update``. To prevent this, ``GLOBAL_FIXED_CLOCK`` provides +the ``accumulated`` and ``fraction``properties. By storing the last frame's position information it is possible +to use ``fraction`` to interpolate towards the next calculated positions. For a visual representation of +this effect look at ``arcade.examples.fixed_update_interpolation``. + +Vertical Synchronization +------------------------ + +What is vertical sync? +^^^^^^^^^^^^^^^^^^^^^^ + +Vertical synchronization (vsync) is a window option in which the +video card is prevented from doing anything visible to the display +memory until after the monitor finishes its current refresh cycle. + +To enable vsync in Arcade:: + + # On window creation + arcade.Window(800, 600, "Window Title", vsync=True) + + # While the application is running + window.set_vsync(True) + +This have advantages and disadvantages depending on the situation. + +Most windows are what we call "double buffered". This means +the window actually has two surfaces. A visible surface and a +hidden surface. All drawing commands will end up in the +hidden surface. When we're done drawing our frame the hidden +and visible surfaces swap places and the new frame is revealed +to the user. + +If this "dance" of swapping surfaces is not timed correctly +with your monitor you might experience small hiccups in movement +or `screen tearing `_. + +Vertical sync disabled as a default +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The Arcade window is by default created with vertical sync +disabled. This is a much safer default for +a number of reasons. + +* In some environments vertical sync is capped to 30 fps. + This can make the game run at half the speed if ``delta_time`` + is not accounted for. We don't expect beginners take + ``delta_time`` into consideration in their projects. +* If threads are used all threads will stall while the + application is waiting for vertical sync + +We cannot guarantee that vertical sync is disabled if +this is enforced on driver level. The vast amount of +driver defaults lets the application control this. \ No newline at end of file diff --git a/doc/programming_guide/gui/concept.rst b/doc/programming_guide/gui/concepts.rst similarity index 100% rename from doc/programming_guide/gui/concept.rst rename to doc/programming_guide/gui/concepts.rst diff --git a/doc/programming_guide/gui/index.rst b/doc/programming_guide/gui/index.rst index cff7e9051a..1cd815bdd8 100644 --- a/doc/programming_guide/gui/index.rst +++ b/doc/programming_guide/gui/index.rst @@ -3,40 +3,25 @@ GUI === -.. figure:: ../../example_code/images/thumbs/gui_flat_button.png - :figwidth: 170px - :target: ../../example_code/how_to_examples/gui_flat_button.html - :ref:`gui_flat_button` - -.. figure:: ../../example_code/images/gui_widgets.png - :figwidth: 170px - :target: ../../example_code/how_to_examples/gui_widgets.html - - :ref:`gui_widgets` - -.. figure:: ../../example_code/images/gui_ok_messagebox.png - :figwidth: 170px - :target: ../../example_code/images/gui_ok_messagebox.html - - :ref:`gui_ok_messagebox` +Arcade's GUI module provides you classes to interact with the user using buttons, labels and much more. -.. figure:: ../../example_code/images/gui_scrollable_text.png - :figwidth: 170px - :target: ../../example_code/images/gui_scrollable_text.html +Behind the scenes the GUI uses a different rendering system than the rest of the engine. +The GUI is especially designed to allow resizing and scaling of the widgets, +which can cause problems with the normal rendering system. - :ref:`gui_scrollable_text` +Usage examples are listed under :ref:`gui_examples_overview`. -Arcade's GUI module provides you classes to interact with the user using buttons, labels and much more. +We recommend to read the :ref:`gui_concepts`, to get a better understanding of the +GUI module and its components. -Using those classes is way easier if the general concepts are known. -It is recommended to read through them. .. toctree:: - :maxdepth: 1 + :maxdepth: 2 + + concepts + layouts + style - concept - style - troubleshooting diff --git a/doc/programming_guide/gui/layouts.rst b/doc/programming_guide/gui/layouts.rst new file mode 100644 index 0000000000..ccf465baea --- /dev/null +++ b/doc/programming_guide/gui/layouts.rst @@ -0,0 +1,126 @@ +.. _gui_layouts: + +GUI Layouts +----------- + +Included Layouts +================ + +The GUI module provides a way to layout your GUI elements in a structured way. +Layouts dynamically resize the elements based on the size of the window. + +The layouts are an optional part of the GUI, but highly recommended to use. +Mixing self positioning and layout positioning is possible, but can lead to unexpected results. + +Layouts apply their layouting right before the rendering phase, +so the layout is always up-to-date for the rendering, +but will not be consistent after instantiation in your ``__init__()`` method. + +To circumvent this, you can trigger a layout run by calling the `UIManager.execute_layout()`. + + +The following layouts are available: + +- :class:`arcade.gui.UIBoxLayout` + + The `UIBoxLayout` class is used to arrange widgets in a + horizontal or vertical box layout. Here are some key points to understand: + + 1. **Orientation**: + The layout can be either horizontal or vertical, controlled by the `vertical` parameter. + + 2. **Alignment**: + Widgets can be aligned within the layout using the `align` parameter. + + 3. **Spacing**: + The layout can have spacing between the widgets, controlled by the `space_between` parameter. + + 4. **Size Hints**: + The layout resizes widgets based on their `size_hint`, `size_hint_min` and `size_hint_max`. + + 5. **Size**: + The layout automatically updates its `size_hint_min` based on the minimal + required space by its children after layout phase. + + In summary, `UIBoxLayout` provides a simple way to arrange widgets in a horizontal or + vertical layout, allowing for alignment and spacing between the widgets. + +- :class:`arcade.gui.UIAnchorLayout` + + The `UIAnchorLayout` class is used to arrange widgets + in the center or at the edges of the layout. + All children are independently anchored the specified anchor points. + + Here are some key points to understand: + + 1. **Anchor**: + The widget can be anchored to the center or at the edges of the layout using + the `anchor_x` and `anchor_y` parameters. In addition to the anchor point, + the widget can be offset from the anchor point using the `offset_x` and `offset_y` parameters. + + 2. **Padding**: + The layout can have padding to ensure spacing to the borders, + controlled by the `padding` parameter. + + 3. **Size**: + The `UIAnchorLayout` is the only layout which by default fills the whole available space. + (Default `size_hint=(1, 1)`) + + 3. **Size Hints**: + The layout resizes widgets based on their `size_hint`, `size_hint_min` and `size_hint_max`. + + In summary, `UIAnchorLayout` provides a way to anchor widgets to a position within the layout. + This allows for flexible positioning of widgets within the layout. + + +- :class:`arcade.gui.UIGridLayout` + + The `UIGridLayout` class is used to arrange widgets in a grid format. Here are some key points to understand: + + 1. **Grid Structure**: + The layout is divided into a specified number of columns and rows. Widgets can be placed in these grid cells. + + 2. **Spanning**: + Widgets can span multiple columns and/or rows using the `column_span` and `row_span` parameters. + + 3. **Dynamic Sizing**: + Widgets can provide a `size_hint` to request dynamic space relative to the layout size. + This means that the widget can grow or shrink based on the available space in the layout. + + 4. **Alignment**: + Widgets can be aligned within their grid cells using the `align_horizontal` and `align_vertical` parameters. + + 5. **Spacing**: + The layout can have horizontal and vertical spacing between the grid cells, + controlled by the `horizontal_spacing` and `vertical_spacing` parameters. + + 6. **Size**: + The layout automatically updates its `size_hint_min` based on the minimal + required space by its children after layouting. + + 7. **Size Hints**: + The layout resizes widgets based on their `size_hint`, `size_hint_min` and `size_hint_max`. + + In summary, `UIGridLayout` provides a flexible way to arrange widgets in a grid, + allowing for dynamic resizing and alignment based on the layout's size + and the widgets' size hints. + +When to use which layout? +========================= + +Choosing the right layout depends on the desired layout structure. +But often there are multiple ways to achieve the same layout. + +Here are some guidelines to help you choose the right layout: + +- Use `UIAnchorLayout` for anchoring widgets to a position within the layout. + This is mostly useful to position widgets freely within the bounds of the layout. + Commonly used as the root layout for the whole GUI. + +- Use `UIBoxLayout` for simple horizontal or vertical layouts. + This is useful for arranging widgets in a row or column. + Multiple `UIBoxLayout` can be nested to create more complex layouts. + +- Use `UIGridLayout` for arranging widgets in a grid format. + This is useful for creating a grid of widgets, where columns and rows should each have a fixed size. + diff --git a/doc/programming_guide/gui/troubleshooting.rst b/doc/programming_guide/gui/troubleshooting.rst deleted file mode 100644 index 4e0c2cb3ca..0000000000 --- a/doc/programming_guide/gui/troubleshooting.rst +++ /dev/null @@ -1,14 +0,0 @@ - -.. _gui_troubleshooting: - -Troubleshooting & Hints -^^^^^^^^^^^^^^^^^^^^^^^ - -:class:`UILabel` does not show the text after it was updated -............................................................ - -Currently the size of :class:`UILabel` is not updated after modifying the text. -Due to the missing information, if the size was set by the user before, this behaviour is intended for now. -To adjust the size to fit the text you can use :meth:`UILabel.fit_content`. - -In the future this might be fixed. diff --git a/doc/programming_guide/resource_handlers.rst b/doc/programming_guide/resource_handlers.rst index 50200025a5..ea09b910e4 100644 --- a/doc/programming_guide/resource_handlers.rst +++ b/doc/programming_guide/resource_handlers.rst @@ -101,7 +101,7 @@ Adding Your Own Resource Handles You may want to define your own resource handles for various reasons. The :py:func:`arcade.resources.add_resource_handle` function allows you -to do this. However, this functionrequires you to first find the absolute +to do this. However, this function requires you to first find the absolute path of the folder you would like to add. @@ -341,7 +341,7 @@ build command. When the executable is ran, the files and folders are unbundled and placed inside a temporary location, (on Window's this is normally ``C:\Users\UserName\AppData\Local\Temp``). This includes an exact copy of your data directory and it is from here that your application is ran from. To ensure that the running executable correctly finds this data directory, -we can use the ``__file__`` dunder variable to locate temporary folder's location. +we can use the ``__file__`` variable to locate temporary folder's location. .. code-block:: Python 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/doc/tutorials/menu/menu_03.py b/doc/tutorials/menu/menu_03.py index 563f5e19a2..44ceca7eff 100644 --- a/doc/tutorials/menu/menu_03.py +++ b/doc/tutorials/menu/menu_03.py @@ -3,6 +3,7 @@ Shows the usage of almost every gui widget, switching views and making a modal. """ + import arcade import arcade.gui @@ -43,14 +44,14 @@ def on_hide_view(self): self.manager.disable() def on_show_view(self): - """ This is run once when we switch to this view """ + """This is run once when we switch to this view""" arcade.set_background_color(arcade.color.DARK_BLUE_GRAY) # Enable the UIManager when the view is showm. self.manager.enable() def on_draw(self): - """ Render the screen. """ + """Render the screen.""" # Clear the screen self.clear() @@ -74,14 +75,16 @@ def __init__(self, main_view): exit = arcade.gui.UIFlatButton(text="Exit", width=320) # Initialise a grid in which widgets can be arranged. - self.grid = arcade.gui.UIGridLayout(column_count=2, row_count=3, horizontal_spacing=20, vertical_spacing=20) + self.grid = arcade.gui.UIGridLayout( + column_count=2, row_count=3, horizontal_spacing=20, vertical_spacing=20 + ) # Adding the buttons to the layout. - self.grid.add(resume, col_num=0, row_num=0) - self.grid.add(start_new_game, col_num=1, row_num=0) - self.grid.add(volume, col_num=0, row_num=1) - self.grid.add(options, col_num=1, row_num=1) - self.grid.add(exit, col_num=0, row_num=2, col_span=2) + self.grid.add(resume, column=0, row=0) + self.grid.add(start_new_game, column=1, row=0) + self.grid.add(volume, column=0, row=1) + self.grid.add(options, column=1, row=1) + self.grid.add(exit, column=0, row=2, column_span=2) self.anchor = self.manager.add(arcade.gui.UIAnchorLayout()) @@ -98,7 +101,7 @@ def on_hide_view(self): self.manager.disable() def on_show_view(self): - """ This is run once when we switch to this view """ + """This is run once when we switch to this view""" # Makes the background darker arcade.set_background_color([rgb - 50 for rgb in arcade.color.DARK_BLUE_GRAY]) @@ -107,7 +110,7 @@ def on_show_view(self): self.manager.enable() def on_draw(self): - """ Render the screen. """ + """Render the screen.""" # Clear the screen self.clear() self.manager.draw() diff --git a/doc/tutorials/menu/menu_04.py b/doc/tutorials/menu/menu_04.py index b0fdd37b14..19edd5518d 100644 --- a/doc/tutorials/menu/menu_04.py +++ b/doc/tutorials/menu/menu_04.py @@ -3,6 +3,7 @@ Shows the usage of almost every gui widget, switching views and making a modal. """ + import arcade import arcade.gui @@ -43,14 +44,14 @@ def on_hide_view(self): self.manager.disable() def on_show_view(self): - """ This is run once when we switch to this view """ + """This is run once when we switch to this view""" arcade.set_background_color(arcade.color.DARK_BLUE_GRAY) # Enable the UIManager when the view is showm. self.manager.enable() def on_draw(self): - """ Render the screen. """ + """Render the screen.""" # Clear the screen self.clear() @@ -74,14 +75,16 @@ def __init__(self, main_view): exit_button = arcade.gui.UIFlatButton(text="Exit", width=320) # Initialise a grid in which widgets can be arranged. - self.grid = arcade.gui.UIGridLayout(column_count=2, row_count=3, horizontal_spacing=20, vertical_spacing=20) + self.grid = arcade.gui.UIGridLayout( + column_count=2, row_count=3, horizontal_spacing=20, vertical_spacing=20 + ) # Adding the buttons to the layout. - self.grid.add(resume_button, col_num=0, row_num=0) - self.grid.add(start_new_game_button, col_num=1, row_num=0) - self.grid.add(volume_button, col_num=0, row_num=1) - self.grid.add(options_button, col_num=1, row_num=1) - self.grid.add(exit_button, col_num=0, row_num=2, col_span=2) + self.grid.add(resume_button, column=0, row=0) + self.grid.add(start_new_game_button, column=1, row=0) + self.grid.add(volume_button, column=0, row=1) + self.grid.add(options_button, column=1, row=1) + self.grid.add(exit_button, column=0, row=2, column_span=2) self.anchor = self.manager.add(arcade.gui.UIAnchorLayout()) @@ -111,25 +114,19 @@ def on_click_exit_button(event): @volume_button.event("on_click") def on_click_volume_button(event): volume_menu = SubMenu() - self.manager.add( - volume_menu, - layer=1 - ) + self.manager.add(volume_menu, layer=1) @options_button.event("on_click") def on_click_options_button(event): options_menu = SubMenu() - self.manager.add( - options_menu, - layer=1 - ) + self.manager.add(options_menu, layer=1) def on_hide_view(self): # Disable the UIManager when the view is hidden. self.manager.disable() def on_show_view(self): - """ This is run once when we switch to this view """ + """This is run once when we switch to this view""" # Makes the background darker arcade.set_background_color([rgb - 50 for rgb in arcade.color.DARK_BLUE_GRAY]) @@ -138,7 +135,7 @@ def on_show_view(self): self.manager.enable() def on_draw(self): - """ Render the screen. """ + """Render the screen.""" # Clear the screen self.clear() self.manager.draw() @@ -147,7 +144,9 @@ def on_draw(self): class SubMenu(arcade.gui.UIMouseFilterMixin, arcade.gui.UIAnchorLayout): """Acts like a fake view/window.""" - def __init__(self, ): + def __init__( + self, + ): super().__init__(size_hint=(1, 1)) # Setup frame which will act like the window. @@ -155,15 +154,17 @@ def __init__(self, ): frame.with_padding(all=20) # Add a background to the window. - frame.with_background(texture=arcade.gui.NinePatchTexture( - left=7, - right=7, - bottom=7, - top=7, - texture=arcade.load_texture( - ":resources:gui_basic_assets/window/dark_blue_gray_panel.png" + frame.with_background( + texture=arcade.gui.NinePatchTexture( + left=7, + right=7, + bottom=7, + top=7, + texture=arcade.load_texture( + ":resources:gui_basic_assets/window/dark_blue_gray_panel.png" + ), ) - )) + ) back_button = arcade.gui.UIFlatButton(text="Back", width=250) # The type of event listener we used earlier for the button will not work here. diff --git a/doc/tutorials/menu/menu_05.py b/doc/tutorials/menu/menu_05.py index 3b7e0181cc..b532552906 100644 --- a/doc/tutorials/menu/menu_05.py +++ b/doc/tutorials/menu/menu_05.py @@ -3,6 +3,7 @@ Shows the usage of almost every gui widget, switching views and making a modal. """ + from typing import List import arcade @@ -45,14 +46,14 @@ def on_hide_view(self): self.manager.disable() def on_show_view(self): - """ This is run once when we switch to this view """ + """This is run once when we switch to this view""" arcade.set_background_color(arcade.color.DARK_BLUE_GRAY) # Enable the UIManager when the view is showm. self.manager.enable() def on_draw(self): - """ Render the screen. """ + """Render the screen.""" # Clear the screen self.clear() @@ -76,14 +77,16 @@ def __init__(self, main_view): exit_button = arcade.gui.UIFlatButton(text="Exit", width=320) # Initialise a grid in which widgets can be arranged. - self.grid = arcade.gui.UIGridLayout(column_count=2, row_count=3, horizontal_spacing=20, vertical_spacing=20) + self.grid = arcade.gui.UIGridLayout( + column_count=2, row_count=3, horizontal_spacing=20, vertical_spacing=20 + ) # Adding the buttons to the layout. - self.grid.add(resume_button, col_num=0, row_num=0) - self.grid.add(start_new_game_button, col_num=1, row_num=0) - self.grid.add(volume_button, col_num=0, row_num=1) - self.grid.add(options_button, col_num=1, row_num=1) - self.grid.add(exit_button, col_num=0, row_num=2, col_span=2) + self.grid.add(resume_button, column=0, row=0) + self.grid.add(start_new_game_button, column=1, row=0) + self.grid.add(volume_button, column=0, row=1) + self.grid.add(options_button, column=1, row=1) + self.grid.add(exit_button, column=0, row=2, column_span=2) self.anchor = self.manager.add(arcade.gui.UIAnchorLayout()) @@ -113,33 +116,31 @@ def on_click_exit_button(event): @volume_button.event("on_click") def on_click_volume_button(event): volume_menu = SubMenu( - "Volume Menu", "How do you like your volume?", "Enable Sound", + "Volume Menu", + "How do you like your volume?", + "Enable Sound", ["Play: Rock", "Play: Punk", "Play: Pop"], "Adjust Volume", ) - self.manager.add( - volume_menu, - layer=1 - ) + self.manager.add(volume_menu, layer=1) @options_button.event("on_click") def on_click_options_button(event): options_menu = SubMenu( - "Funny Menu", "Too much fun here", "Fun?", + "Funny Menu", + "Too much fun here", + "Fun?", ["Make Fun", "Enjoy Fun", "Like Fun"], "Adjust Fun", ) - self.manager.add( - options_menu, - layer=1 - ) + self.manager.add(options_menu, layer=1) def on_hide_view(self): # Disable the UIManager when the view is hidden. self.manager.disable() def on_show_view(self): - """ This is run once when we switch to this view """ + """This is run once when we switch to this view""" # Makes the background darker arcade.set_background_color([rgb - 50 for rgb in arcade.color.DARK_BLUE_GRAY]) @@ -148,7 +149,7 @@ def on_show_view(self): self.manager.enable() def on_draw(self): - """ Render the screen. """ + """Render the screen.""" # Clear the screen self.clear() self.manager.draw() @@ -157,7 +158,14 @@ def on_draw(self): class SubMenu(arcade.gui.UIMouseFilterMixin, arcade.gui.UIAnchorLayout): """Acts like a fake view/window.""" - def __init__(self, title: str, input_text: str, toggle_label: str, dropdown_options: List[str], slider_label: str): + def __init__( + self, + title: str, + input_text: str, + toggle_label: str, + dropdown_options: List[str], + slider_label: str, + ): super().__init__(size_hint=(1, 1)) # Setup frame which will act like the window. @@ -166,15 +174,17 @@ def __init__(self, title: str, input_text: str, toggle_label: str, dropdown_opti # Add a background to the window. # Nine patch smoothes the edges. - frame.with_background(texture=arcade.gui.NinePatchTexture( - left=7, - right=7, - bottom=7, - top=7, - texture=arcade.load_texture( - ":resources:gui_basic_assets/window/dark_blue_gray_panel.png" + frame.with_background( + texture=arcade.gui.NinePatchTexture( + left=7, + right=7, + bottom=7, + top=7, + texture=arcade.load_texture( + ":resources:gui_basic_assets/window/dark_blue_gray_panel.png" + ), ) - )) + ) back_button = arcade.gui.UIFlatButton(text="Back", width=250) # The type of event listener we used earlier for the button will not work here. @@ -188,15 +198,14 @@ def __init__(self, title: str, input_text: str, toggle_label: str, dropdown_opti # Load the on-off textures. on_texture = arcade.load_texture(":resources:gui_basic_assets/toggle/circle_switch_on.png") - off_texture = arcade.load_texture(":resources:gui_basic_assets/toggle/circle_switch_off.png") + off_texture = arcade.load_texture( + ":resources:gui_basic_assets/toggle/circle_switch_off.png" + ) # Create the on-off toggle and a label toggle_label = arcade.gui.UILabel(text=toggle_label) toggle = arcade.gui.UITextureToggle( - on_texture=on_texture, - off_texture=off_texture, - width=20, - height=20 + on_texture=on_texture, off_texture=off_texture, width=20, height=20 ) # Align toggle and label horizontally next to each other @@ -205,12 +214,21 @@ def __init__(self, title: str, input_text: str, toggle_label: str, dropdown_opti toggle_group.add(toggle_label) # Create dropdown with a specified default. - dropdown = arcade.gui.UIDropdown(default=dropdown_options[0], options=dropdown_options, height=20, width=250) + dropdown = arcade.gui.UIDropdown( + default=dropdown_options[0], options=dropdown_options, height=20, width=250 + ) slider_label = arcade.gui.UILabel(text=slider_label) - pressed_style = arcade.gui.UISlider.UIStyle(filled_track=arcade.color.GREEN, unfilled_track=arcade.color.RED) + pressed_style = arcade.gui.UISlider.UIStyle( + filled_track=arcade.color.GREEN, unfilled_track=arcade.color.RED + ) default_style = arcade.gui.UISlider.UIStyle() - style_dict = {"press": pressed_style, "normal": default_style, "hover": default_style, "disabled": default_style} + style_dict = { + "press": pressed_style, + "normal": default_style, + "hover": default_style, + "disabled": default_style, + } # Configuring the styles is optional. slider = arcade.gui.UISlider(value=50, width=250, style=style_dict) 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 093a49aaf6..50b65d42a2 100644 --- a/tests/integration/examples/test_line_lengths.py +++ b/tests/integration/examples/test_line_lengths.py @@ -4,6 +4,7 @@ Adapted from util/check_example_line_length.py """ + import re from pathlib import Path import arcade @@ -15,19 +16,20 @@ "examples/procedural_caves_cellular", # Contains long link in header ] + def is_ignored(path: Path): path_str = str(path.as_posix()) for pattern in IGNORE_PATTERNS: if pattern in path_str: - return True + return True return False def test_line_lengths(): - paths = EXAMPLE_ROOT.glob("**/*.py") + paths = EXAMPLE_ROOT.glob("**/*.py") - regex = re.compile("^.{97}.*$") + regex = re.compile("^.{100}.*$") grand_total = 0 file_count = 0 @@ -40,11 +42,11 @@ def test_line_lengths(): with open(path, encoding="utf8") as f: for line in f: line_no += 1 - result = regex.search(line) + result = regex.search(line.strip("\r")) if result: print(f" {path.relative_to(EXAMPLE_ROOT)}:{line_no}: " + line.strip()) grand_total += 1 # print(f"{grand_total} across {file_count} files.") if grand_total > 0: - raise AssertionError(f"{grand_total} lines exceed length limit in examples") + raise AssertionError(f"{grand_total} lines exceed length limit in examples") diff --git a/tests/unit/camera/test_viewport_projector.py b/tests/unit/camera/test_viewport_projector.py new file mode 100644 index 0000000000..2e965d81d1 --- /dev/null +++ b/tests/unit/camera/test_viewport_projector.py @@ -0,0 +1,25 @@ +import pytest as pytest + +from pyglet.math import Vec3, Vec2 + +from arcade import camera, Window +from arcade.types import Point, LBWH, Rect + +@pytest.mark.parametrize("wrld_pos", [Vec2(100, 150), Vec2(1280, 720), Vec3(500, 500, -10)]) +def test_viewport_projector_project(window: Window, wrld_pos: Point): + cam = camera.default.ViewportProjector() + assert cam.project(wrld_pos) == wrld_pos.xy + +@pytest.mark.parametrize("wrld_pos", [Vec2(100, 150), Vec2(1280, 720), Vec3(500, 500, -10)]) +def test_viewport_projector_unproject(window: Window, wrld_pos: Point): + cam = camera.default.ViewportProjector() + x, y, *z = wrld_pos + + assert cam.unproject(wrld_pos) == Vec3(x, y, 0.0 if not z else z[0]) + +@pytest.mark.parametrize("viewport", [LBWH(0.0, 0.0, 100, 200), LBWH(100, 100, 20, 40), LBWH(300, 20, 20, 700)]) +def test_viewport_projector_viewport(window: Window, viewport: Rect): + cam = camera.default.ViewportProjector() + assert cam.viewport.viewport == window.ctx.viewport + cam.viewport = viewport + assert cam.viewport == viewport diff --git a/tests/unit/gui/__init__.py b/tests/unit/gui/__init__.py index 43a5404ed5..d2de0281bd 100644 --- a/tests/unit/gui/__init__.py +++ b/tests/unit/gui/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations # allow |-type hinting in Python 3.9 + from abc import abstractmethod from contextlib import contextmanager from typing import List @@ -11,26 +13,34 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.event_history: List[UIEvent] = [] - def move_mouse(self, x: int, y: int): + def move_mouse(self, x: int | float, y: int | float): self.on_mouse_motion(x, y, 0, 0) - def click_and_hold(self, x: int, y: int, button=arcade.MOUSE_BUTTON_LEFT): + def click_and_hold(self, x: int | float, y: int | float, button=arcade.MOUSE_BUTTON_LEFT): self.on_mouse_press(x=x, y=y, button=button, modifiers=0) - def drag(self, x: int, y: int, dx=0.0, dy=0.0, buttons=arcade.MOUSE_BUTTON_LEFT, modifiers=0): + def drag( + self, + x: int | float, + y: int | float, + dx=0.0, + dy=0.0, + buttons=arcade.MOUSE_BUTTON_LEFT, + modifiers=0, + ): self.on_mouse_drag(x=x, y=y, dx=dx, dy=dy, buttons=buttons, modifiers=modifiers) - def release(self, x: int, y: int, button=arcade.MOUSE_BUTTON_LEFT): + def release(self, x: int | float, y: int | float, button=arcade.MOUSE_BUTTON_LEFT): self.on_mouse_release(x=x, y=y, button=button, modifiers=0) def type_text(self, text: str): self.on_text(text) - def click(self, x: int, y: int, button=arcade.MOUSE_BUTTON_LEFT): + def click(self, x: int | float, y: int | float, button=arcade.MOUSE_BUTTON_LEFT): self.click_and_hold(x, y, button=button) self.release(x, y, button=button) - def right_click(self, x: int, y: int): + def right_click(self, x: int | float, y: int | float): self.click_and_hold(x, y, button=arcade.MOUSE_BUTTON_RIGHT) self.release(x, y, button=arcade.MOUSE_BUTTON_RIGHT) diff --git a/tests/unit/gui/conftest.py b/tests/unit/gui/conftest.py index c6cc98131b..a34f64cd99 100644 --- a/tests/unit/gui/conftest.py +++ b/tests/unit/gui/conftest.py @@ -11,5 +11,5 @@ def __init__(self, *args, **kwargs): @fixture -def uimanager(window) -> InteractionUIManager: +def ui(window) -> InteractionUIManager: return InteractionUIManager() diff --git a/tests/unit/gui/test_interactions.py b/tests/unit/gui/test_interactions.py index 96cd0570c3..a6cce78256 100644 --- a/tests/unit/gui/test_interactions.py +++ b/tests/unit/gui/test_interactions.py @@ -7,42 +7,42 @@ from . import record_ui_events -def test_hover_on_widget(uimanager): +def test_hover_on_widget(ui): # GIVEN widget = UIDummy() - uimanager.add(widget) + ui.add(widget) # WHEN - uimanager.move_mouse(widget.center_x, widget.center_y) + ui.move_mouse(widget.center_x, widget.center_y) # THEN assert widget.hovered is True -def test_overlapping_hover_on_widget(uimanager): +def test_overlapping_hover_on_widget(ui): # GIVEN widget1 = UIDummy() widget2 = UIDummy() - uimanager.add(widget1) - uimanager.add(widget2) + ui.add(widget1) + ui.add(widget2) # WHEN - uimanager.move_mouse(widget1.center_x, widget1.center_y) + ui.move_mouse(widget1.center_x, widget1.center_y) # THEN assert widget1.hovered is True assert widget2.hovered is True -def test_left_click_on_widget(uimanager): +def test_left_click_on_widget(ui): # GIVEN widget1 = UIDummy() widget1.on_click = Mock() - uimanager.add(widget1) + ui.add(widget1) # WHEN with record_ui_events(widget1, "on_event", "on_click") as records: - uimanager.click(widget1.center_x, widget1.center_y, button=arcade.MOUSE_BUTTON_LEFT) + ui.click(widget1.center_x, widget1.center_y, button=arcade.MOUSE_BUTTON_LEFT) # THEN records: List[UIEvent] @@ -61,15 +61,15 @@ def test_left_click_on_widget(uimanager): assert widget1.on_click.called -def test_ignores_right_click_on_widget(uimanager): +def test_ignores_right_click_on_widget(ui): # GIVEN widget1 = UIDummy() widget1.on_click = Mock() - uimanager.add(widget1) + ui.add(widget1) # WHEN with record_ui_events(widget1, "on_event", "on_click") as records: - uimanager.click(widget1.center_x, widget1.center_y, button=arcade.MOUSE_BUTTON_RIGHT) + ui.click(widget1.center_x, widget1.center_y, button=arcade.MOUSE_BUTTON_RIGHT) # THEN records: List[UIEvent] @@ -79,16 +79,16 @@ def test_ignores_right_click_on_widget(uimanager): assert not widget1.on_click.called -def test_click_on_widget_if_disabled(uimanager): +def test_click_on_widget_if_disabled(ui): # GIVEN widget1 = UIDummy() widget1.disabled = True widget1.on_click = Mock() - uimanager.add(widget1) + ui.add(widget1) # WHEN with record_ui_events(widget1, "on_event", "on_click") as records: - uimanager.click(widget1.center_x, widget1.center_y) + ui.click(widget1.center_x, widget1.center_y) # THEN records: List[UIEvent] @@ -99,17 +99,17 @@ def test_click_on_widget_if_disabled(uimanager): assert not widget1.on_click.called -def test_click_on_overlay_widget_consumes_events(uimanager): +def test_click_on_overlay_widget_consumes_events(ui): # GIVEN widget1 = UIDummy() widget2 = UIDummy() - uimanager.add(widget1) - uimanager.add(widget2) + ui.add(widget1) + ui.add(widget2) # WHEN with record_ui_events(widget1, "on_click") as w1_records: with record_ui_events(widget2, "on_click") as w2_records: - uimanager.click(widget1.center_x, widget1.center_y) + ui.click(widget1.center_x, widget1.center_y) # THEN # events are consumed before they get to underlying widget @@ -126,17 +126,17 @@ def test_click_on_overlay_widget_consumes_events(uimanager): assert click_event.y == widget2.center_y -def test_click_consumed_by_nested_widget(uimanager): +def test_click_consumed_by_nested_widget(ui): # GIVEN widget1 = UIDummy() widget2 = UIDummy() widget1.add(widget2) - uimanager.add(widget1) + ui.add(widget1) # WHEN with record_ui_events(widget1, "on_click") as w1_records: with record_ui_events(widget2, "on_click") as w2_records: - uimanager.click(widget1.center_x, widget1.center_y) + ui.click(widget1.center_x, widget1.center_y) # THEN # events are consumed before they get to underlying widget diff --git a/tests/unit/gui/test_layouting_anchorlayout.py b/tests/unit/gui/test_layouting_anchorlayout.py index b15dea5027..4714f82123 100644 --- a/tests/unit/gui/test_layouting_anchorlayout.py +++ b/tests/unit/gui/test_layouting_anchorlayout.py @@ -29,7 +29,11 @@ def test_place_widget(window): def test_place_widget_relative_to_own_content_rect(window): dummy = UIDummy(width=100, height=200) - subject = UIAnchorLayout(x=0, y=0, width=500, height=500).with_border(width=2).with_padding(left=50, top=100) + subject = ( + UIAnchorLayout(x=0, y=0, width=500, height=500) + .with_border(width=2) + .with_padding(left=50, top=100) + ) subject.add( dummy, @@ -104,7 +108,9 @@ def test_grow_child_to_max_size(window): def test_shrink_child_to_min_size(window): subject = UIAnchorLayout(width=400, height=400) - dummy = subject.add(UIDummy(width=100, height=100, size_hint=(0.1, 0.1), size_hint_min=(200, 150))) + dummy = subject.add( + UIDummy(width=100, height=100, size_hint=(0.1, 0.1), size_hint_min=(200, 150)) + ) subject._do_layout() @@ -112,21 +118,36 @@ def test_shrink_child_to_min_size(window): assert dummy.size == Vec2(200, 150) -def test_grow_child_within_bounds(window): +def test_children_can_grow_out_of_bounce(window): + """This tests behavior, which is used for scrolling.""" subject = UIAnchorLayout(width=400, height=400) dummy = subject.add(UIDummy(width=100, height=100, size_hint=(2, 2))) subject._do_layout() + assert subject.rect == LBWH(0, 0, 400, 400) + assert dummy.size == Vec2(800, 800) + + +def test_children_limited_to_layout_size_when_enforced(window): + """This tests behavior, which is used for scrolling.""" + subject = UIAnchorLayout(width=400, height=400) + subject._restrict_child_size = True + dummy = subject.add(UIDummy(width=100, height=100, size_hint=(2, 2))) + + subject._do_layout() + assert subject.rect == LBWH(0, 0, 400, 400) assert dummy.size == Vec2(400, 400) def test_only_adjust_size_if_size_hint_is_given_for_dimension(window): subject = UIAnchorLayout(width=400, height=400) - dummy = subject.add(UIDummy(width=100, height=100, size_hint=(2, None), size_hint_min=(None, 200))) + dummy = subject.add( + UIDummy(width=100, height=100, size_hint=(2, None), size_hint_min=(None, 200)) + ) subject._do_layout() assert subject.rect == LBWH(0, 0, 400, 400) - assert dummy.size == Vec2(400, 100) + assert dummy.size == Vec2(800, 100) diff --git a/tests/unit/gui/test_layouting_box_main_algorithm.py b/tests/unit/gui/test_layouting_box_main_algorithm.py new file mode 100644 index 0000000000..0f28e9adf0 --- /dev/null +++ b/tests/unit/gui/test_layouting_box_main_algorithm.py @@ -0,0 +1,219 @@ +from pytest import approx + +from arcade.gui import UIWidget +from arcade.gui.widgets.layout import _C, _box_axis_algorithm + + +def test_constraint_from_widget_no_sh(): + widget = UIWidget(size_hint=None, width=100) + + assert _C.from_widget_width(widget) == _C(hint=0, min=100, max=100) + + +def test_constraint_from_widget_no_shm(): + widget = UIWidget(size_hint=(1, 1), width=100, size_hint_min=None) + + assert _C.from_widget_width(widget) == _C(hint=1, min=0, max=None) + + +def test_shw_smaller_1(window): + # GIVEN + entries = [ + _C(hint=0.1, min=0, max=None), + _C(hint=0.1, min=0, max=None), + _C(hint=0.5, min=0, max=None), + ] + + # WHEN + sizes = _box_axis_algorithm(entries, 100) + + # THEN + assert sizes == [10, 10, 50] + + +def test_complex_example_with_max_value(): + # GIVEN + entries = [ + _C(hint=0.2, min=0, max=None), + _C(hint=0.2, min=0, max=None), + _C(hint=0.6, min=0, max=50), + ] + + # WHEN + sizes = _box_axis_algorithm(entries, 100) + + # THEN + assert sizes == [20, 20, 50] + + +def test_complex_example_with_min_value(): + # GIVEN + entries = [ + _C(hint=0.3, min=0, max=None), + _C(hint=0.3, min=0, max=None), + _C(hint=0.1, min=50, max=None), + ] + + # WHEN + sizes = _box_axis_algorithm(entries, 100) + + # THEN + assert sizes == [25, 25, 50] + + +def test_issue_example_with_min_value(): + # GIVEN + entries = [ + _C(hint=0.2, min=0, max=None), + _C(hint=0.2, min=0, max=None), + _C(hint=0.3, min=20, max=None), + ] + + # WHEN + sizes = _box_axis_algorithm(entries, 100) + + # THEN + assert sizes == [20, 20, 30] + + +def test_complex_example_hint_above_1(): + # GIVEN + entries = [ + _C(hint=0.4, min=0, max=None), + _C(hint=0.4, min=0, max=None), + _C(hint=0.4, min=0, max=None), + ] + + # WHEN + e1, e2, e3 = _box_axis_algorithm(entries, 100) + + # THEN + assert e1 == approx(33.33, rel=0.01) + assert e2 == approx(33.33, rel=0.01) + assert e3 == approx(33.33, rel=0.01) + + +def test_complex_example_hint_above_1_with_min(): + # GIVEN + entries = [ + _C(hint=0.4, min=0, max=None), + _C(hint=0.4, min=0, max=None), + _C(hint=0.4, min=50, max=None), + ] + + # WHEN + sizes = _box_axis_algorithm(entries, 100) + + # THEN + assert sizes == [25, 25, 50] + + +def test_more_complex_example(): + # GIVEN + entries = [ + _C(hint=0.4, min=0, max=None), + _C(hint=0.4, min=50, max=None), + _C(hint=0.4, min=50, max=None), + ] + + # WHEN + sizes = _box_axis_algorithm(entries, 100) + + # THEN + assert sizes == [0, 50, 50] + + +def test_min_greater_total(): + # GIVEN + entries = [ + _C(hint=0.2, min=10, max=None), + _C(hint=0.2, min=50, max=None), + _C(hint=0.2, min=50, max=None), + ] + + # WHEN + sizes = _box_axis_algorithm(entries, 100) + + # THEN + assert sizes == [10, 50, 50] + + +def test_size_hint_is_0(): + # GIVEN + entries = [ + _C(hint=0, min=0, max=None), + _C(hint=0.5, min=50, max=None), + _C(hint=0.5, min=50, max=None), + ] + + # WHEN + sizes = _box_axis_algorithm(entries, 100) + + # THEN + assert sizes == [0, 50, 50] + + +def test_example_without_hint(): + # GIVEN + entries = [ + _C(hint=0, min=10, max=None), + _C(hint=0, min=50, max=None), + _C(hint=0, min=50, max=None), + ] + + # WHEN + sizes = _box_axis_algorithm(entries, 100) + + # THEN + assert sizes == [10, 50, 50] + + +def test_example_grow_relative_to_size_hint(): + # GIVEN + entries = [ + _C(hint=1, min=25, max=None), + _C(hint=0.5, min=25, max=None), + ] + + # WHEN + e1, e2 = _box_axis_algorithm(entries, 100) + + # THEN + assert [int(e1), int(e2)] == [ + 66, + 33, + ] + + +def test_example_grow_relative_to_size_hint_no_min(): + # GIVEN + entries = [ + _C(hint=1, min=0, max=None), + _C(hint=0.5, min=0, max=None), + ] + + # WHEN + e1, e2 = _box_axis_algorithm(entries, 100) + + # THEN + assert [int(e1), int(e2)] == [ + 66, + 33, + ] + + +def test_example_grow_relative_to_size_hint_huge_min(): + # GIVEN + entries = [ + _C(hint=0.75, min=0, max=None), + _C(hint=0.5, min=80, max=None), + ] + + # WHEN + e1, e2 = _box_axis_algorithm(entries, 100) + + # THEN + assert [int(e1), int(e2)] == [ + 20, + 80, + ] diff --git a/tests/unit/gui/test_layouting_box_ortho_algorithm.py b/tests/unit/gui/test_layouting_box_ortho_algorithm.py new file mode 100644 index 0000000000..55727996c9 --- /dev/null +++ b/tests/unit/gui/test_layouting_box_ortho_algorithm.py @@ -0,0 +1,46 @@ +from arcade.gui.widgets.layout import _C, _box_axis_algorithm, _box_orthogonal_algorithm + + +def test_simple_values(window): + # GIVEN + entries = [ + _C(hint=0.1, min=0, max=None), + _C(hint=0.1, min=0, max=None), + _C(hint=0.5, min=0, max=None), + ] + + # WHEN + positions = _box_orthogonal_algorithm(entries, 100) + + # THEN + assert positions == [10, 10, 50] + + +def test_issue_example_with_min_value(): + # GIVEN + entries = [ + _C(hint=0.2, min=0, max=None), + _C(hint=0.2, min=0, max=None), + _C(hint=0.3, min=40, max=None), + ] + + # WHEN + positions = _box_orthogonal_algorithm(entries, 100) + + # THEN + assert positions == [20, 20, 40] + + +def test_issue_example_with_max_value(): + # GIVEN + entries = [ + _C(hint=0.2, min=0, max=None), + _C(hint=0.2, min=0, max=None), + _C(hint=1, min=0, max=50), + ] + + # WHEN + positions = _box_orthogonal_algorithm(entries, 100) + + # THEN + assert positions == [20, 20, 50] diff --git a/tests/unit/gui/test_layouting_boxlayout.py b/tests/unit/gui/test_layouting_boxlayout.py index 29bd5294a6..377823fb31 100644 --- a/tests/unit/gui/test_layouting_boxlayout.py +++ b/tests/unit/gui/test_layouting_boxlayout.py @@ -315,8 +315,8 @@ def test_vertical_resize_children_according_size_hint(window): box._do_layout() assert box.size == Vec2(300, 400) - assert dummy_1.size == Vec2(300, approx(100 + 200 / 3 * 2)) - assert dummy_2.size == Vec2(150, approx(100 + 200 / 3 * 1)) + assert dummy_1.size == Vec2(300, approx(400 / 3 * 2)) + assert dummy_2.size == Vec2(150, approx(400 / 3 * 1)) def test_vertical_ignores_size_hint_none(window): @@ -369,8 +369,8 @@ def test_horizontal_resize_children_according_size_hint(window): box._do_layout() assert box.size == Vec2(300, 400) - assert dummy_1.size == Vec2(approx(100 + 100 / 3 * 2), 400) - assert dummy_2.size == Vec2(approx(100 + 100 / 3 * 1), 200) + assert dummy_1.size == Vec2(box.width * 2 / 3, 400) + assert dummy_2.size == Vec2(box.width * 1 / 3, 200) def test_horizontal_ignores_size_hint_none(window): @@ -432,3 +432,73 @@ def test_children_change_size_hint_min(window): # TODO test size hint < 1 (do not take full width) + + +def test_children_size_hint_sum_below_1(window): + ui = UIManager() + box1 = UIBoxLayout(width=100, height=100, vertical=False, size_hint=None) + dummy_1 = UIDummy(width=10, height=10, size_hint=(0.2, 1)) + dummy_2 = UIDummy(width=10, height=10, size_hint=(0.5, 1)) + + ui.add(box1) + box1.add(dummy_1) + box1.add(dummy_2) + + ui.execute_layout() + + assert box1.size == Vec2(100, 100) + assert dummy_1.size == Vec2(20, 100) + assert dummy_2.size == Vec2(50, 100) + + +def test_children_size_hint_sum_below_1_with_shm(window): + ui = UIManager() + box1 = UIBoxLayout(width=100, height=100, vertical=False, size_hint=None) + dummy_1 = UIDummy(width=10, height=10, size_hint=(0.2, 1)) + dummy_2 = UIDummy(width=10, height=10, size_hint=(0.5, 1), size_hint_min=(40, 100)) + + ui.add(box1) + box1.add(dummy_1) + box1.add(dummy_2) + + ui.execute_layout() + + assert box1.size == Vec2(100, 100) + assert dummy_1.size == Vec2(20, 100) + assert dummy_2.size == Vec2(50, 100) + + +def test_children_size_hint_sum_below_1_with_shm_to_big(window): + """size_hint_min of second child requests more space, then would be available.""" + ui = UIManager() + box1 = UIBoxLayout(width=100, height=100, vertical=False, size_hint=None) + dummy_1 = UIDummy(width=10, height=10, size_hint=(0.2, 1)) + dummy_2 = UIDummy(width=10, height=10, size_hint=(0.5, 1), size_hint_min=(90, 100)) + + ui.add(box1) + box1.add(dummy_1) + box1.add(dummy_2) + + ui.execute_layout() + + assert box1.size == Vec2(100, 100) + assert dummy_1.size == Vec2(10, 100) + assert dummy_2.size == Vec2(90, 100) + + +def test_children_size_hint_sum_above_1(window): + """Children get less space than requested. but relative to their size hints.""" + ui = UIManager() + box1 = UIBoxLayout(width=100, height=100, vertical=False, size_hint=None) + dummy_1 = UIDummy(width=10, height=10, size_hint=(0.5, 1)) + dummy_2 = UIDummy(width=10, height=10, size_hint=(0.6, 1)) + + ui.add(box1) + box1.add(dummy_1) + box1.add(dummy_2) + + ui.execute_layout() + + assert box1.size == Vec2(100, 100) + assert dummy_1.width == approx(45.45, rel=0.01) + assert dummy_2.width == approx(54.55, rel=0.01) diff --git a/tests/unit/gui/test_layouting_gridlayout.py b/tests/unit/gui/test_layouting_gridlayout.py index 27199669c5..281f0b0bba 100644 --- a/tests/unit/gui/test_layouting_gridlayout.py +++ b/tests/unit/gui/test_layouting_gridlayout.py @@ -1,10 +1,11 @@ +from pyglet.math import Vec2 + from arcade import LBWH -from arcade.gui import UIDummy, UIManager, UIBoxLayout, UIAnchorLayout +from arcade.gui import UIAnchorLayout, UIBoxLayout, UIDummy, UIManager from arcade.gui.widgets.layout import UIGridLayout -from pyglet.math import Vec2 -def test_place_widget(window): +def test_place_widget(ui): dummy1 = UIDummy(width=100, height=100) dummy2 = UIDummy(width=100, height=100) dummy3 = UIDummy(width=100, height=100) @@ -17,8 +18,8 @@ def test_place_widget(window): subject.add(dummy3, 1, 0) subject.add(dummy4, 1, 1) - subject.rect = LBWH(0, 0, *subject.size_hint_min) - subject.do_layout() + ui.add(subject) + ui.execute_layout() # check that do_layout doesn't manipulate the rect assert subject.rect == LBWH(0, 0, 200, 200) @@ -29,15 +30,15 @@ def test_place_widget(window): assert dummy4.position == Vec2(100, 0) -def test_can_handle_empty_cells(window): +def test_can_handle_empty_cells(ui): dummy1 = UIDummy(width=100, height=100) subject = UIGridLayout(column_count=2, row_count=2) subject.add(dummy1, 0, 0) - subject.rect = LBWH(0, 0, *subject.size_hint_min) - subject.do_layout() + ui.add(subject) + ui.execute_layout() # check that do_layout doesn't manipulate the rect assert subject.rect == LBWH(0, 0, 100, 100) @@ -45,7 +46,7 @@ def test_can_handle_empty_cells(window): assert dummy1.position == Vec2(0, 0) -def test_place_widget_with_different_sizes(window): +def test_place_widget_with_different_sizes(ui): dummy1 = UIDummy(width=50, height=100) dummy2 = UIDummy(width=100, height=100) dummy3 = UIDummy(width=100, height=50) @@ -58,8 +59,8 @@ def test_place_widget_with_different_sizes(window): subject.add(dummy3, 1, 0) subject.add(dummy4, 1, 1) - subject.rect = LBWH(0, 0, *subject.size_hint_min) - subject.do_layout() + ui.add(subject) + ui.execute_layout() assert subject.rect == LBWH(0, 0, 200, 200) @@ -69,22 +70,21 @@ def test_place_widget_with_different_sizes(window): assert dummy4.position == Vec2(125, 25) -def test_place_widget_within_content_rect(window): +def test_place_widget_within_content_rect(ui): dummy1 = UIDummy(width=100, height=100) subject = UIGridLayout(column_count=1, row_count=1).with_padding(left=10, bottom=20) subject.add(dummy1, 0, 0) - assert subject.size_hint_min == (110, 120) - - subject.rect = LBWH(0, 0, *subject.size_hint_min) - subject.do_layout() + ui.add(subject) + ui.execute_layout() + assert subject.size_hint_min == (110, 120) assert dummy1.position == Vec2(10, 20) -def test_place_widgets_with_col_row_span(window): +def test_place_widgets_with_col_row_span(ui): dummy1 = UIDummy(width=100, height=100) dummy2 = UIDummy(width=100, height=100) dummy3 = UIDummy(width=100, height=100) @@ -101,11 +101,11 @@ def test_place_widgets_with_col_row_span(window): subject.add(dummy2, 0, 1) subject.add(dummy3, 1, 0) subject.add(dummy4, 1, 1) - subject.add(dummy5, 0, 2, col_span=2) + subject.add(dummy5, 0, 2, column_span=2) subject.add(dummy6, 2, 0, row_span=3) - subject.rect = LBWH(0, 0, *subject.size_hint_min) - subject.do_layout() + ui.add(subject) + ui.execute_layout() assert dummy1.position == Vec2(0, 200) assert dummy2.position == Vec2(0, 100) @@ -115,7 +115,20 @@ def test_place_widgets_with_col_row_span(window): assert dummy6.position == Vec2(200, 50) -def test_place_widgets_with_col_row_span_and_spacing(window): +def test_place_widgets_with_col_row_span_and_spacing(ui): + """ + col1 col2 + +-----+-----+ + | 1 | 2 | + +-----+-----+ + | 3 | 4 | + +-----+-----+ + | 6 | + +-----+-----+ + + col1 width: 100 + col2 width: 100 + """ dummy1 = UIDummy(width=100, height=100) dummy2 = UIDummy(width=100, height=100) dummy3 = UIDummy(width=100, height=100) @@ -129,22 +142,23 @@ def test_place_widgets_with_col_row_span_and_spacing(window): ) subject.add(dummy1, 0, 0) - subject.add(dummy2, 0, 1) - subject.add(dummy3, 1, 0) + subject.add(dummy2, 1, 0) + subject.add(dummy3, 0, 1) subject.add(dummy4, 1, 1) - subject.add(dummy5, 0, 2, col_span=2) + subject.add(dummy5, 0, 2, column_span=2) - subject.rect = LBWH(0, 0, *subject.size_hint_min) - subject.do_layout() + ui.add(subject) + ui.execute_layout() - assert dummy1.position == Vec2(10, 200) - assert dummy2.position == Vec2(10, 100) - assert dummy3.position == Vec2(130, 200) - assert dummy4.position == Vec2(130, 100) - assert dummy5.position == Vec2(10, 0) + assert subject.rect.size == (220, 300) + assert dummy1.position == Vec2(0, 200) + assert dummy2.position == Vec2(120, 200) + assert dummy3.position == Vec2(0, 100) + assert dummy4.position == Vec2(120, 100) + assert dummy5.position == Vec2(0, 0) -def test_fit_content_by_default(window): +def test_fit_content_by_default(ui): subject = UIGridLayout( column_count=1, row_count=1, @@ -153,11 +167,13 @@ def test_fit_content_by_default(window): assert subject.size_hint == (0, 0) -def test_adjust_children_size_relative(window): - dummy1 = UIDummy(width=100, height=100) - dummy2 = UIDummy(width=50, height=50, size_hint=(0.75, 0.75)) - dummy3 = UIDummy(width=100, height=100, size_hint=(0.5, 0.5), size_hint_min=(60, 60)) - dummy4 = UIDummy(width=100, height=100) +def test_adjust_children_size_relative(ui): + dummy1 = UIDummy(width=50, height=50) # fix size + dummy2 = UIDummy(width=50, height=50, size_hint=(0.75, 0.75)) # shrinks + dummy3 = UIDummy( + width=100, height=100, size_hint=(0.3, 0.3), size_hint_min=(60, 60) + ) # shrinks to 60,60 + dummy4 = UIDummy(width=10, height=10) # fix size subject = UIGridLayout( column_count=2, @@ -169,19 +185,20 @@ def test_adjust_children_size_relative(window): subject.add(dummy3, 1, 0) subject.add(dummy4, 1, 1) - subject.rect = LBWH(0, 0, *subject.size_hint_min) - subject.do_layout() - - # check that do_layout doesn't manipulate the rect - assert subject.rect == LBWH(0, 0, 200, 200) + ui.add(subject) + ui.execute_layout() - assert dummy1.size == Vec2(100, 100) - assert dummy2.size == Vec2(75, 75) + assert subject.rect.size == (110, 70) + assert dummy1.size == Vec2(50, 50) + assert dummy2.size == Vec2( + 50, # width: 75% of 110 is 82, but cell is only 50, so it should be 50 + 10, # height: 75% of 70 is 52, but cell is only 10, so it should be 10 + ) assert dummy3.size == Vec2(60, 60) - assert dummy4.size == Vec2(100, 100) + assert dummy4.size == Vec2(10, 10) -def test_does_not_adjust_children_without_size_hint(window): +def test_does_not_adjust_children_without_size_hint(ui): dummy1 = UIDummy(width=100, height=100) dummy2 = UIDummy(width=50, height=50, size_hint=(0.75, None)) dummy3 = UIDummy(width=50, height=50, size_hint=(None, 0.75)) @@ -197,19 +214,19 @@ def test_does_not_adjust_children_without_size_hint(window): subject.add(dummy3, 1, 0) subject.add(dummy4, 1, 1) - subject.rect = LBWH(0, 0, *subject.size_hint_min) - subject.do_layout() + ui.add(subject) + ui.execute_layout() # check that do_layout doesn't manipulate the rect assert subject.rect == LBWH(0, 0, 200, 200) assert dummy1.size == Vec2(100, 100) - assert dummy2.size == Vec2(75, 50) - assert dummy3.size == Vec2(50, 75) + assert dummy2.size == Vec2(100, 50) + assert dummy3.size == Vec2(50, 100) assert dummy4.size == Vec2(100, 100) -def test_size_hint_and_spacing(window): +def test_size_hint_and_spacing(ui): dummy1 = UIDummy(width=100, height=100) subject = UIGridLayout( @@ -221,8 +238,8 @@ def test_size_hint_and_spacing(window): subject.add(dummy1, 0, 0) - subject.rect = LBWH(0, 0, *subject.size_hint_min) - subject.do_layout() + ui.add(subject) + ui.execute_layout() assert dummy1.size == Vec2(100, 100) @@ -230,7 +247,7 @@ def test_size_hint_and_spacing(window): assert dummy1.size == Vec2(100, 100) -def test_empty_cells(window): +def test_empty_cells(ui): dummy1 = UIDummy(width=100, height=100) subject = UIGridLayout( @@ -240,14 +257,13 @@ def test_empty_cells(window): subject.add(dummy1, 2, 2) - subject.rect = LBWH(0, 0, *subject.size_hint_min) - subject.do_layout() + ui.add(subject) + ui.execute_layout() assert dummy1.position == Vec2(0, 0) -def test_nested_grid_layouts(window): - ui = UIManager() +def test_nested_grid_layouts(ui): outer = UIGridLayout(row_count=1, column_count=1) inner = UIGridLayout(row_count=1, column_count=1) @@ -261,8 +277,7 @@ def test_nested_grid_layouts(window): assert outer.rect.size == Vec2(100, 100) -def test_nested_box_layouts(window): - ui = UIManager() +def test_nested_box_layouts(ui): outer = UIGridLayout(row_count=1, column_count=1) inner = UIBoxLayout() @@ -276,8 +291,7 @@ def test_nested_box_layouts(window): assert outer.rect.size == Vec2(100, 100) -def test_nested_anchor_layouts(window): - ui = UIManager() +def test_nested_anchor_layouts(ui): outer = UIGridLayout(row_count=1, column_count=1) inner = UIAnchorLayout(size_hint_min=(100, 100)) @@ -290,8 +304,7 @@ def test_nested_anchor_layouts(window): assert outer.rect.size == Vec2(100, 100) -def test_update_size_hint_min_on_child_size_change(window): - ui = UIManager() +def test_update_size_hint_min_on_child_size_change(ui): grid = UIGridLayout(row_count=1, column_count=1) dummy = UIDummy(size_hint_min=(100, 100), size_hint=(0, 0)) @@ -303,3 +316,67 @@ def test_update_size_hint_min_on_child_size_change(window): assert dummy.rect.size == Vec2(200, 200) assert grid.rect.size == Vec2(200, 200) + + +def test_widgets_are_centered(ui): + # grid elements not centered + # https://github.com/pythonarcade/arcade/issues/2210 + grid = UIGridLayout(row_count=1, column_count=1, horizontal_spacing=10, vertical_spacing=10) + ui.add(grid) + + dummy1 = UIDummy(width=100, height=100) + grid.add(dummy1, 0, 0) + + ui.execute_layout() + + assert dummy1.rect.bottom_left == Vec2(0, 0) + + +def test_size_hint_none(ui): + # size changed when sh None + grid = UIGridLayout(row_count=1, column_count=1, width=150, height=150, size_hint=None) + ui.add(grid) + + dummy1 = UIDummy(width=100, height=100, size_hint_max=(150, None)) + grid.add(dummy1, 0, 0) + + ui.execute_layout() + + assert dummy1.rect.size == Vec2(100, 100) + + +def test_minimal_size(ui): + grid = ui.add( + UIGridLayout( + column_count=3, + row_count=1, + size_hint=(0, 0), + ) + ) + + grid.add(UIDummy(width=200, height=100), column=0, row=0, column_span=2) + grid.add(UIDummy(width=100, height=100), column=2, row=0, row_span=1) + + ui.execute_layout() + + assert grid.size == (300, 100) + assert grid.size_hint_min == (300, 100) + + +def test_calculate_size_hint_min(ui): + dummy1 = UIDummy(width=50, height=100) + dummy2 = UIDummy(width=100, height=100) + dummy3 = UIDummy(width=100, height=50) + dummy4 = UIDummy(width=50, height=50) + + subject = UIGridLayout(column_count=2, row_count=2) + + subject.add(dummy1, 0, 0) + subject.add(dummy2, 0, 1) + subject.add(dummy3, 1, 0) + subject.add(dummy4, 1, 1) + + ui.add(subject) + ui.execute_layout() + + assert subject.size_hint_min == (200, 200) diff --git a/tests/unit/gui/test_ninepatch.py b/tests/unit/gui/test_ninepatch.py index 6cc6853a9a..32501981ee 100644 --- a/tests/unit/gui/test_ninepatch.py +++ b/tests/unit/gui/test_ninepatch.py @@ -6,12 +6,6 @@ from arcade.gui import NinePatchTexture -# ":resources:gui_basic_assets/button_square_blue_pressed.png" -# ":resources:gui_basic_assets/button_square_blue.png" -# ":resources:gui_basic_assets/red_button_hover.png" -# ":resources:gui_basic_assets/red_button_normal.png" - - @pytest.fixture(scope="module") def texture(): return arcade.load_texture( @@ -59,7 +53,7 @@ def test_borders_too_big(ctx, texture): def test_swap_texture(ctx, texture): patch = NinePatchTexture(texture=texture, left=7, right=8, bottom=9, top=10) new_texture = arcade.load_texture( - ":resources:gui_basic_assets/red_button_normal.png", + ":resources:gui_basic_assets/button/red_normal.png", ) patch.draw_rect(rect=LBWH(0, 0, 200, 200)) patch.texture = new_texture diff --git a/tests/unit/gui/test_uilabel.py b/tests/unit/gui/test_uilabel.py index 7363a1a83c..0753ff68f9 100644 --- a/tests/unit/gui/test_uilabel.py +++ b/tests/unit/gui/test_uilabel.py @@ -11,7 +11,9 @@ def test_constructor_only_text_no_size(window): """Should fit text""" label = UILabel(text="Example") - assert label.rect.width == pytest.approx(63, abs=7) # on windows the width differs about 6 pixel + assert label.rect.width == pytest.approx( + 63, abs=10 + ) # on windows the width differs about 6 pixel assert label.rect.height == pytest.approx(19, abs=1) @@ -46,21 +48,21 @@ def test_constructor_adaptive_width_support_for_multiline_text(window): def test_with_border_keeps_previous_size(window): label = UILabel(text="Example") - assert label.rect.width == pytest.approx(63, abs=7) + assert label.rect.width == pytest.approx(63, abs=10) assert label.rect.height == pytest.approx(19, abs=6) label.with_border() - assert label.rect.width == pytest.approx(63, abs=7) + assert label.rect.width == pytest.approx(63, abs=10) assert label.rect.height == pytest.approx(19, abs=6) def test_with_padding_keeps_previous_size(window): label = UILabel(text="Example") - assert label.rect.width == pytest.approx(63, abs=7) + assert label.rect.width == pytest.approx(63, abs=10) assert label.rect.height == pytest.approx(19, abs=6) label.with_padding(all=2) - assert label.rect.width == pytest.approx(63, abs=7) + assert label.rect.width == pytest.approx(63, abs=10) assert label.rect.height == pytest.approx(19, abs=6) @@ -177,7 +179,7 @@ def test_multiline_enabled_size_hint_min_adapts_to_new_text(window): assert label.size_hint_min[1] > shm_h -def test_integration_with_layout_fit_to_content(uimanager): +def test_integration_with_layout_fit_to_content(ui): """Tests multiple integrations with layout/uimanager and auto size. Just to be sure, it really works as expected. @@ -187,16 +189,16 @@ def test_integration_with_layout_fit_to_content(uimanager): size_hint=(0, 0), # default, enables auto size ) - uimanager.add(label) - uimanager.execute_layout() + ui.add(label) + ui.execute_layout() # auto size should fit the text - assert label.rect.width == pytest.approx(63, abs=7) + assert label.rect.width == pytest.approx(63, abs=10) assert label.rect.height == pytest.approx(19, abs=6) # even when text changed label.text = "Example, which is way longer" - uimanager.execute_layout() + ui.execute_layout() assert label.rect.width > 63 assert label.rect.height == pytest.approx(19, abs=6) @@ -204,13 +206,13 @@ def test_integration_with_layout_fit_to_content(uimanager): # or font label.text = "Example" label.update_font(font_size=20) - uimanager.execute_layout() + ui.execute_layout() assert label.rect.width > 63 assert label.rect.height > 20 -def test_fit_content_overrides_width(uimanager): +def test_fit_content_overrides_width(ui): label = UILabel( text="Example", width=100, @@ -219,11 +221,11 @@ def test_fit_content_overrides_width(uimanager): label.fit_content() - assert label.rect.width == pytest.approx(63, abs=7) + assert label.rect.width == pytest.approx(63, abs=10) assert label.rect.height == pytest.approx(19, abs=6) -def test_fit_content_uses_adaptive_multiline_width(uimanager): +def test_fit_content_uses_adaptive_multiline_width(ui): label = UILabel( text="Example with multiline enabled", width=70, diff --git a/tests/unit/gui/test_uimanager_callbacks.py b/tests/unit/gui/test_uimanager_callbacks.py index 199ae47a45..edfa10293e 100644 --- a/tests/unit/gui/test_uimanager_callbacks.py +++ b/tests/unit/gui/test_uimanager_callbacks.py @@ -13,11 +13,11 @@ from . import record_ui_events -def test_on_mouse_press_passes_an_event(uimanager): - uimanager.add(UIDummy()) +def test_on_mouse_press_passes_an_event(ui): + ui.add(UIDummy()) - with record_ui_events(uimanager, "on_event") as records: - uimanager.on_mouse_press(1, 2, 3, 4) + with record_ui_events(ui, "on_event") as records: + ui.on_mouse_press(1, 2, 3, 4) event = records[-1] assert isinstance(event, UIMousePressEvent) @@ -27,11 +27,11 @@ def test_on_mouse_press_passes_an_event(uimanager): assert event.modifiers == 4 -def test_on_mouse_release_passes_an_event(uimanager): - uimanager.add(UIDummy()) +def test_on_mouse_release_passes_an_event(ui): + ui.add(UIDummy()) - with record_ui_events(uimanager, "on_event") as records: - uimanager.on_mouse_release(1, 2, 3, 4) + with record_ui_events(ui, "on_event") as records: + ui.on_mouse_release(1, 2, 3, 4) event = records[-1] assert isinstance(event, UIMouseReleaseEvent) @@ -41,11 +41,11 @@ def test_on_mouse_release_passes_an_event(uimanager): assert event.modifiers == 4 -def test_on_mouse_scroll_passes_an_event(uimanager): - uimanager.add(UIDummy()) +def test_on_mouse_scroll_passes_an_event(ui): + ui.add(UIDummy()) - with record_ui_events(uimanager, "on_event") as records: - uimanager.on_mouse_scroll(1, 2, 3, 4) + with record_ui_events(ui, "on_event") as records: + ui.on_mouse_scroll(1, 2, 3, 4) event = records[-1] assert isinstance(event, UIMouseScrollEvent) @@ -55,11 +55,11 @@ def test_on_mouse_scroll_passes_an_event(uimanager): assert event.scroll_y == 4 -def test_on_mouse_motion_passes_an_event(uimanager): - uimanager.add(UIDummy()) +def test_on_mouse_motion_passes_an_event(ui): + ui.add(UIDummy()) - with record_ui_events(uimanager, "on_event") as records: - uimanager.on_mouse_motion(1, 2, 3, 4) + with record_ui_events(ui, "on_event") as records: + ui.on_mouse_motion(1, 2, 3, 4) event = records[-1] assert isinstance(event, UIMouseMovementEvent) @@ -69,11 +69,11 @@ def test_on_mouse_motion_passes_an_event(uimanager): assert event.dy == 4 -def test_on_key_press_passes_an_event(uimanager): - uimanager.add(UIDummy()) +def test_on_key_press_passes_an_event(ui): + ui.add(UIDummy()) - with record_ui_events(uimanager, "on_event") as records: - uimanager.on_key_press(arcade.key.ENTER, 0) + with record_ui_events(ui, "on_event") as records: + ui.on_key_press(arcade.key.ENTER, 0) event = records[-1] assert isinstance(event, UIKeyPressEvent) @@ -81,11 +81,11 @@ def test_on_key_press_passes_an_event(uimanager): assert event.modifiers == 0 -def test_on_key_release_passes_an_event(uimanager): - uimanager.add(UIDummy()) +def test_on_key_release_passes_an_event(ui): + ui.add(UIDummy()) - with record_ui_events(uimanager, "on_event") as records: - uimanager.on_key_release(arcade.key.ENTER, 0) + with record_ui_events(ui, "on_event") as records: + ui.on_key_release(arcade.key.ENTER, 0) event = records[-1] assert isinstance(event, UIKeyReleaseEvent) @@ -93,33 +93,33 @@ def test_on_key_release_passes_an_event(uimanager): assert event.modifiers == 0 -def test_on_text_passes_an_event(uimanager): - uimanager.add(UIDummy()) +def test_on_text_passes_an_event(ui): + ui.add(UIDummy()) - with record_ui_events(uimanager, "on_event") as records: - uimanager.on_text("a") + with record_ui_events(ui, "on_event") as records: + ui.on_text("a") event = records[-1] assert isinstance(event, UITextInputEvent) assert event.text == "a" -def test_on_text_motion_passes_an_event(uimanager): - uimanager.add(UIDummy()) +def test_on_text_motion_passes_an_event(ui): + ui.add(UIDummy()) - with record_ui_events(uimanager, "on_event") as records: - uimanager.on_text_motion(MOTION_UP) + with record_ui_events(ui, "on_event") as records: + ui.on_text_motion(MOTION_UP) event = records[-1] assert isinstance(event, UITextMotionEvent) assert event.motion == MOTION_UP -def test_on_text_motion_selection_passes_an_event(uimanager): - uimanager.add(UIDummy()) +def test_on_text_motion_selection_passes_an_event(ui): + ui.add(UIDummy()) - with record_ui_events(uimanager, "on_event") as records: - uimanager.on_text_motion_select(MOTION_UP) + with record_ui_events(ui, "on_event") as records: + ui.on_text_motion_select(MOTION_UP) event = records[-1] assert isinstance(event, UITextMotionSelectEvent) diff --git a/tests/unit/gui/test_uimanager_camera.py b/tests/unit/gui/test_uimanager_camera.py index cb21b60629..93a67b734b 100644 --- a/tests/unit/gui/test_uimanager_camera.py +++ b/tests/unit/gui/test_uimanager_camera.py @@ -5,58 +5,58 @@ from arcade.gui import UIFlatButton -def test_ui_manager_respects_window_camera(uimanager, window): +def test_ui_manager_respects_window_camera(ui, window): # GIVEN in_game_cam = arcade.Camera2D(viewport=LBWH(100, 100, window.width, window.height)) - button = uimanager.add(UIFlatButton(text="BottomLeftButton", width=100, height=100)) + button = ui.add(UIFlatButton(text="BottomLeftButton", width=100, height=100)) button.on_click = Mock() # WHEN in_game_cam.use() - uimanager.click(50, 50) + ui.click(50, 50) # THEN assert button.on_click.called -def test_ui_manager_use_positioned_camera(uimanager, window): +def test_ui_manager_use_positioned_camera(ui, window): # GIVEN - button = uimanager.add(UIFlatButton(text="BottomLeftButton", width=100, height=100)) + button = ui.add(UIFlatButton(text="BottomLeftButton", width=100, height=100)) button.on_click = Mock() # WHEN # this moves the camera bottom left and UI elements are shown more to the top right - uimanager.camera.bottom_left = (-100, -100) - uimanager.click(150, 150) + ui.camera.bottom_left = (-100, -100) + ui.click(150, 150) # THEN assert button.on_click.called -def test_ui_manager_use_rotated_camera(uimanager, window): +def test_ui_manager_use_rotated_camera(ui, window): # GIVEN - button = uimanager.add(UIFlatButton(text="BottomLeftButton", width=100, height=100)) + button = ui.add(UIFlatButton(text="BottomLeftButton", width=100, height=100)) button.on_click = Mock() # WHEN - uimanager.camera.angle = 90 - x, y = uimanager.camera.project((50, 50)) - uimanager.click(x, y) + ui.camera.angle = 90 + x, y = ui.camera.project((50, 50)) + ui.click(x, y) # THEN - assert button.on_click.called, (uimanager.camera.project((50, 50)), window.size) + assert button.on_click.called, (ui.camera.project((50, 50)), window.size) -def test_ui_manager_use_zoom_camera(uimanager, window): +def test_ui_manager_use_zoom_camera(ui, window): # GIVEN - button = uimanager.add(UIFlatButton(text="BottomLeftButton", width=100, height=100)) + button = ui.add(UIFlatButton(text="BottomLeftButton", width=100, height=100)) button.on_click = Mock() # WHEN - uimanager.camera.zoom = 0.9 - x, y = uimanager.camera.project((50, 50)) - uimanager.click(x, y) + ui.camera.zoom = 0.9 + x, y = ui.camera.project((50, 50)) + ui.click(x, y) # THEN assert button.on_click.called diff --git a/tests/unit/gui/test_uislider.py b/tests/unit/gui/test_uislider.py index dd8affb2ab..8c4e5b415c 100644 --- a/tests/unit/gui/test_uislider.py +++ b/tests/unit/gui/test_uislider.py @@ -11,17 +11,17 @@ def test_initial_value_set(): assert slider.value == 0 -def test_change_value_on_drag(uimanager): +def test_change_value_on_drag(ui): # GIVEN slider = UISlider(height=30, width=120) - uimanager.add(slider) + ui.add(slider) assert slider.value == 0 # WHEN cx, cy = slider._thumb_x, slider.rect.y - uimanager.click_and_hold(cx, cy) - uimanager.drag(cx + 20, cy) + ui.click_and_hold(cx, cy) + ui.drag(cx + 20, cy) # THEN assert slider.value == 20 diff --git a/tests/unit/gui/test_widget_inputtext.py b/tests/unit/gui/test_widget_inputtext.py index 9455cbf02e..55f4eddb62 100644 --- a/tests/unit/gui/test_widget_inputtext.py +++ b/tests/unit/gui/test_widget_inputtext.py @@ -1,7 +1,7 @@ -from arcade.gui import UIInputText +from arcade.gui import UIInputText, UIOnChangeEvent -def test_deactivated_by_default(uimanager): +def test_deactivated_by_default(ui): # GIVEN widget = UIInputText() @@ -9,51 +9,96 @@ def test_deactivated_by_default(uimanager): assert widget.active is False -def test_activated_after_click(uimanager): +def test_activated_after_click(ui): # GIVEN widget = UIInputText() - uimanager.add(widget) + ui.add(widget) # WHEN - uimanager.click(*widget.rect.center) + ui.click(*widget.rect.center) # THEN assert widget.active is True -def test_deactivated_after_off_click(uimanager): +def test_deactivated_after_off_click(ui): # GIVEN widget = UIInputText() - uimanager.add(widget) + ui.add(widget) widget.activate() # WHEN - uimanager.click(200, 200) + ui.click(200, 200) # THEN assert widget.active is False -def test_captures_text_when_active(uimanager): +def test_captures_text_when_active(ui): # GIVEN widget = UIInputText() - uimanager.add(widget) + ui.add(widget) widget.activate() # WHEN - uimanager.type_text("Hello") + ui.type_text("Hello") # THEN assert widget.text == "Hello" -def test_does_not_capture_text_when_inactive(uimanager): +def test_does_not_capture_text_when_inactive(ui): # GIVEN widget = UIInputText() - uimanager.add(widget) + ui.add(widget) # WHEN - uimanager.type_text("Hello") + ui.type_text("Hello") # THEN assert widget.text == "" + + +def test_dispatches_on_change_event(ui): + # GIVEN + widget = UIInputText() + ui.add(widget) + + recorded = [] + + @widget.event("on_change") + def on_change(event): + recorded.append(event) + + # WHEN + widget.activate() + ui.type_text("Hello") + + # THEN + assert len(recorded) == 1 + + recorded_event = recorded[0] + assert isinstance(recorded_event, UIOnChangeEvent) + assert recorded_event.new_value == "Hello" + + +def test_setting_text_dispatches_on_change_event(ui): + # GIVEN + widget = UIInputText() + ui.add(widget) + + recorded = [] + + @widget.event("on_change") + def on_change(event): + recorded.append(event) + + # WHEN + widget.text = "Hello" + + # THEN + assert len(recorded) == 1 + + recorded_event = recorded[0] + assert isinstance(recorded_event, UIOnChangeEvent) + assert recorded_event.new_value == "Hello" 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", + ]