diff --git a/arcade/gui/constructs.py b/arcade/gui/constructs.py index 75899f399d..256bb99ad6 100644 --- a/arcade/gui/constructs.py +++ b/arcade/gui/constructs.py @@ -1,6 +1,4 @@ -""" -Constructs, are prepared widget combinations, you can use for common use-cases -""" +"""Constructs, are prepared widget combinations, you can use for common use-cases""" from __future__ import annotations @@ -16,8 +14,7 @@ class UIMessageBox(UIMouseFilterMixin, UIAnchorLayout): - """ - A simple dialog box that pops up a message with buttons to close. + """A simple dialog box that pops up a message with buttons to close. Subclass this class or overwrite the 'on_action' event handler with .. code-block:: python @@ -27,11 +24,12 @@ class UIMessageBox(UIMouseFilterMixin, UIAnchorLayout): def on_action(event: UIOnActionEvent): pass + Args: + width: Width of the message box + height: Height of the message box + message_text: Text to show as message to the user + buttons: List of strings, which are shown as buttons - :param width: Width of the message box - :param height: Height of the message box - :param message_text: Text to show as message to the user - :param buttons: List of strings, which are shown as buttons """ def __init__( @@ -100,19 +98,20 @@ def on_action(self, event: UIOnActionEvent): class UIButtonRow(UIBoxLayout): - """ - Places buttons in a row. - - :param vertical: Whether the button row is vertical or not. - :param align: Where to align the button row. - :param size_hint: Tuple of floats (0.0 - 1.0) of how much space of the parent - should be requested. - :param size_hint_min: Min width and height in pixel. - :param size_hint_max: Max width and height in pixel. - :param space_between: The space between the children. - :param style: Not used. - :param Tuple[str, ...] button_labels: The labels for the buttons. - :param callback: The callback function which will receive the text of the clicked button. + """Places buttons in a row. + + Args: + vertical: Whether the button row is vertical or not. + align: Where to align the button row. + size_hint: Tuple of floats (0.0 - 1.0) of how much space of the + parent should be requested. + size_hint_min: Min width and height in pixel. + size_hint_max: Max width and height in pixel. + space_between: The space between the children. + callback: The callback function which will receive the text of + the clicked button. + button_factory: The factory to create the buttons. Default is py:class:`UIFlatButton`. + **kwargs: Passed to UIBoxLayout """ def __init__( @@ -124,8 +123,8 @@ def __init__( size_hint_min: Optional[Any] = None, size_hint_max: Optional[Any] = None, space_between: int = 10, - style: Optional[Any] = None, button_factory: type = UIFlatButton, + **kwargs, ): super().__init__( vertical=vertical, @@ -134,7 +133,7 @@ def __init__( size_hint_min=size_hint_min, size_hint_max=size_hint_max, space_between=space_between, - style=style, + **kwargs, ) self.register_event_type("on_action") # type: ignore # https://github.com/pyglet/pyglet/pull/1173 # noqa @@ -147,12 +146,20 @@ def add_button( style=None, multiline=False, ): + """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. + """ button = self.button_factory(text=label, style=style, multiline=multiline) button.on_click = self._on_click # type: ignore self.add(button) return button def on_action(self, event: UIOnActionEvent): + """Called when button was pressed, override this method to handle button presses.""" pass def _on_click(self, event: UIOnClickEvent): diff --git a/arcade/gui/events.py b/arcade/gui/events.py index cdb1eae23d..03bb97bbf8 100644 --- a/arcade/gui/events.py +++ b/arcade/gui/events.py @@ -3,13 +3,25 @@ from dataclasses import dataclass from typing import Any +from pyglet.math import Vec2 + @dataclass class UIEvent: - """ - An event created by the GUI system. Can be passed using widget.dispatch("on_event", event). - An event always has a source, which is the UIManager for general input events, - but will be the specific widget in case of events like on_click events. + """An event created by the GUI system. + + Can be passed as follows: + + .. code-block:: python + + widget.dispatch("on_event", event) + + An event always has a source. This is the UIManager for general input + events, but will be the specific widget in case of events like click + events. + + Args: + source: The source of the event. """ source: Any @@ -17,21 +29,30 @@ class UIEvent: @dataclass class UIMouseEvent(UIEvent): - """ - Covers all mouse event + """Base class for all UI mouse events. + + Args: + x: The x coordinate of the mouse. + y: The y coordinate of the mouse. """ x: int y: int @property - def pos(self): - return self.x, self.y + def pos(self) -> Vec2: + """Return the position as tuple (x, y)""" + return Vec2(self.x, self.y) @dataclass class UIMouseMovementEvent(UIMouseEvent): - """Triggered when the mouse is moved.""" + """Triggered when the mouse is moved. + + Args: + dx: The change in x coordinate. + dy: The change in y coordinate. + """ dx: int dy: int @@ -39,7 +60,12 @@ class UIMouseMovementEvent(UIMouseEvent): @dataclass class UIMousePressEvent(UIMouseEvent): - """Triggered when a mouse button(left, right, middle) is pressed.""" + """Triggered when a mouse button(left, right, middle) is pressed. + + Args: + button: The button pressed. + modifiers: The modifiers pressed. + """ button: int modifiers: int @@ -47,7 +73,14 @@ class UIMousePressEvent(UIMouseEvent): @dataclass class UIMouseDragEvent(UIMouseEvent): - """Triggered when the mouse moves while one of its buttons being pressed.""" + """Triggered when the mouse moves while one of its buttons being pressed. + + Args: + dx: The change in x coordinate. + dy: The change in y coordinate. + buttons: The buttons pressed. + modifiers: The modifiers pressed. + """ dx: int dy: int @@ -57,7 +90,12 @@ class UIMouseDragEvent(UIMouseEvent): @dataclass class UIMouseReleaseEvent(UIMouseEvent): - """Triggered when a mouse button is released.""" + """Triggered when a mouse button is released. + + Args: + button: The button released. + modifiers: The modifiers pressed + """ button: int modifiers: int @@ -65,7 +103,12 @@ class UIMouseReleaseEvent(UIMouseEvent): @dataclass class UIMouseScrollEvent(UIMouseEvent): - """Triggered by rotating the scroll wheel on the mouse.""" + """Triggered by rotating the scroll wheel on the mouse. + + Args: + scroll_x: The horizontal scroll amount. + scroll_y: The vertical scroll + """ scroll_x: int scroll_y: int @@ -73,7 +116,12 @@ class UIMouseScrollEvent(UIMouseEvent): @dataclass class UIKeyEvent(UIEvent): - """Covers all keyboard event.""" + """Base class for all keyboard-centric UI events. + + Args: + symbol: The key pressed. + modifiers: The modifiers pressed. + """ symbol: int modifiers: int @@ -114,6 +162,9 @@ class UITextInputEvent(UITextEvent): * a platform-specific input method, such as pen input on a tablet PC To learn more, see pyglet's `relevant documentation `_. + + Args: + text: The text inputted. Often a single character. """ text: str @@ -121,21 +172,34 @@ class UITextInputEvent(UITextEvent): @dataclass class UITextMotionEvent(UITextEvent): - """Triggered when text cursor moves.""" + """Triggered when text cursor moves. + + Args: + motion: The motion of the cursor + """ motion: Any @dataclass class UITextMotionSelectEvent(UITextEvent): - """Triggered when the text cursor moves selecting the text with it.""" + """Triggered when the text cursor moves selecting the text with it. + + Args: + selection: The selection of the cursor + """ selection: Any @dataclass class UIOnClickEvent(UIMouseEvent): - """Triggered when a widget is clicked.""" + """Triggered when a widget is clicked. + + Args: + button: The button clicked. + modifiers: The modifiers pressed. + """ button: int modifiers: int @@ -143,8 +207,10 @@ class UIOnClickEvent(UIMouseEvent): @dataclass class UIOnUpdateEvent(UIEvent): - """ - Arcade on_update callback passed as :class:`UIEvent` + """Arcade on_update callback passed as :class:`UIEvent`. + + Args: + dt: Time since last update """ dt: int @@ -152,8 +218,11 @@ class UIOnUpdateEvent(UIEvent): @dataclass class UIOnChangeEvent(UIEvent): - """ - Value of a widget changed + """Value of a widget changed. + + Args: + old_value: The old value. + new_value: The new value. """ old_value: Any @@ -162,10 +231,10 @@ class UIOnChangeEvent(UIEvent): @dataclass class UIOnActionEvent(UIEvent): - """ - Notification about an action + """Notification about an action. - :param action: Value describing the action, mostly a string + Args: + action: Value describing the action, mostly a string """ action: Any diff --git a/arcade/gui/examples/__init__.py b/arcade/gui/examples/__init__.py index 3cc16871cf..a451f32b52 100644 --- a/arcade/gui/examples/__init__.py +++ b/arcade/gui/examples/__init__.py @@ -1,5 +1,4 @@ -""" -This package contains GUI specific examples. +"""This package contains GUI specific examples. These show explicit GUI examples, and are not part of the main examples. diff --git a/arcade/gui/examples/anchor_layout.py b/arcade/gui/examples/anchor_layout.py index d6d502ef88..6f069f3d8a 100644 --- a/arcade/gui/examples/anchor_layout.py +++ b/arcade/gui/examples/anchor_layout.py @@ -1,5 +1,4 @@ -""" -Positioning UI elements with UIAnchorLayout +"""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 diff --git a/arcade/gui/examples/box_layout.py b/arcade/gui/examples/box_layout.py index 93b8f61a67..68f98b0d20 100644 --- a/arcade/gui/examples/box_layout.py +++ b/arcade/gui/examples/box_layout.py @@ -1,5 +1,4 @@ -""" -Arrange widgets in vertical or horizontal lines with UIBoxLayout +"""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 diff --git a/arcade/gui/examples/button_with_text.py b/arcade/gui/examples/button_with_text.py index 2fd6065d8e..979ea549e1 100644 --- a/arcade/gui/examples/button_with_text.py +++ b/arcade/gui/examples/button_with_text.py @@ -1,5 +1,4 @@ -""" -Customizing buttons with text & textures. +"""Customizing buttons with text & textures. This example showcases arcade's range of different built-in button types and how they can be used to customize a UI. A UIGridLayout is used to diff --git a/arcade/gui/examples/dropdown.py b/arcade/gui/examples/dropdown.py index 69de3d97e2..c8ee0728b2 100644 --- a/arcade/gui/examples/dropdown.py +++ b/arcade/gui/examples/dropdown.py @@ -1,5 +1,4 @@ -""" -Creating a dropdown menu with UIDropDown +"""Creating a dropdown menu with UIDropDown When an option in the UIDropDown is chosen, this example will respond by changing the text displayed on screen to reflect it. @@ -30,7 +29,7 @@ def __init__(self): self.dropdown.center_on_screen() self.ui.add(self.dropdown) - self.label = self.ui.add(UILabel(text=" ", text_color=(0, 0, 0))) + self.label = self.ui.add(UILabel(text=" ", text_color=arcade.color.BLACK)) @self.dropdown.event() def on_change(event: UIOnChangeEvent): diff --git a/arcade/gui/examples/exp_hidden_password.py b/arcade/gui/examples/exp_hidden_password.py index 1b5b62d16f..129bfd02db 100644 --- a/arcade/gui/examples/exp_hidden_password.py +++ b/arcade/gui/examples/exp_hidden_password.py @@ -1,5 +1,4 @@ -""" -Creating a hidden password field +"""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 diff --git a/arcade/gui/examples/exp_scroll_area.py b/arcade/gui/examples/exp_scroll_area.py index ffaf57e442..8c070e610d 100644 --- a/arcade/gui/examples/exp_scroll_area.py +++ b/arcade/gui/examples/exp_scroll_area.py @@ -1,5 +1,4 @@ -""" -This example is a proof-of-concept for a UIScrollArea. +"""This example is a proof-of-concept for a UIScrollArea. You can currently scroll through the UIScrollArea in the following ways: diff --git a/arcade/gui/examples/grid_layout.py b/arcade/gui/examples/grid_layout.py index ec21749ea5..276f4d4331 100644 --- a/arcade/gui/examples/grid_layout.py +++ b/arcade/gui/examples/grid_layout.py @@ -1,5 +1,4 @@ -""" -Arrange elements in line with grid tiles with UIGridLayout +"""Arrange elements in line with grid tiles with UIGridLayout UIGridLayout allows you to place elements to cover one or more cells of a grid. To assign an element more than one grid square, diff --git a/arcade/gui/examples/gui_and_camera.py b/arcade/gui/examples/gui_and_camera.py index 248a6d80fd..cf1107c2f1 100644 --- a/arcade/gui/examples/gui_and_camera.py +++ b/arcade/gui/examples/gui_and_camera.py @@ -1,5 +1,4 @@ -""" -This example shows how to use arcade.gui with a camera. +"""This example shows how to use arcade.gui with a camera. It is a simple game where the player can move around and collect coins. The player can upgrade their speed and the spawn rate of the coins. The game has a timer and ends after 60 seconds. @@ -23,8 +22,7 @@ class MyCoinGame(UIView): - """ - Main view of the game. This class is a subclass of UIView, which provides + """Main view of the game. This class is a subclass of UIView, which provides basic GUI setup. We add UIManager to the view under `self.ui`. The example showcases how to: diff --git a/arcade/gui/examples/ninepatch.py b/arcade/gui/examples/ninepatch.py index 87207d6c27..22e9623945 100644 --- a/arcade/gui/examples/ninepatch.py +++ b/arcade/gui/examples/ninepatch.py @@ -1,5 +1,4 @@ -""" -Create custom scalable UI themes with NinePatchTexture +"""Create custom scalable UI themes with NinePatchTexture Nine-patch textures are a technique for scalable custom borders and frames for UI elements. Widgets which support a background texture can diff --git a/arcade/gui/examples/side_bars_with_box_layout.py b/arcade/gui/examples/side_bars_with_box_layout.py index b5923cc8ef..4cdfa24c96 100644 --- a/arcade/gui/examples/side_bars_with_box_layout.py +++ b/arcade/gui/examples/side_bars_with_box_layout.py @@ -1,5 +1,4 @@ -""" -Creating sidebar-like layouts with UIBoxLayout +"""Creating sidebar-like layouts with UIBoxLayout This example creates left, right, top, and bottom bars by combining the following: diff --git a/arcade/gui/examples/size_hints.py b/arcade/gui/examples/size_hints.py index 24ffbc26b5..7453609dac 100644 --- a/arcade/gui/examples/size_hints.py +++ b/arcade/gui/examples/size_hints.py @@ -1,5 +1,4 @@ -""" -Sizing widgets using size hint keyword arguments +"""Sizing widgets using size hint keyword arguments The following keyword arguments can be used to set preferred size information for layouts which arrange widgets diff --git a/arcade/gui/examples/stats_topleft.py b/arcade/gui/examples/stats_topleft.py index 9b96da1385..ea6fd5e70f 100644 --- a/arcade/gui/examples/stats_topleft.py +++ b/arcade/gui/examples/stats_topleft.py @@ -1,5 +1,4 @@ -""" -Displaying stats in the window's top left corner +"""Displaying stats in the window's top left corner This example displays numerical stats with labels by using the following: diff --git a/arcade/gui/examples/style_v2.py b/arcade/gui/examples/style_v2.py index 0e9d719933..6f4aab11f2 100644 --- a/arcade/gui/examples/style_v2.py +++ b/arcade/gui/examples/style_v2.py @@ -1,5 +1,4 @@ -""" -Changing UI styles in response to events +"""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 diff --git a/arcade/gui/examples/textured_slider.py b/arcade/gui/examples/textured_slider.py index 0bdb183dec..dcb33cf137 100644 --- a/arcade/gui/examples/textured_slider.py +++ b/arcade/gui/examples/textured_slider.py @@ -1,5 +1,4 @@ -""" -Create a slider using textures. +"""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. diff --git a/arcade/gui/examples/toggle.py b/arcade/gui/examples/toggle.py index 06e0edb3e3..c333aa5be8 100644 --- a/arcade/gui/examples/toggle.py +++ b/arcade/gui/examples/toggle.py @@ -1,5 +1,4 @@ -""" -Use a custom texture for a toggle button. +"""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. diff --git a/arcade/gui/examples/widget_gallery.py b/arcade/gui/examples/widget_gallery.py index 43de14a28e..eac7e7ff58 100644 --- a/arcade/gui/examples/widget_gallery.py +++ b/arcade/gui/examples/widget_gallery.py @@ -1,5 +1,4 @@ -""" -A combination of multiple widgets from other examples +"""A combination of multiple widgets from other examples See the other GUI examples for more information. diff --git a/arcade/gui/experimental/__init__.py b/arcade/gui/experimental/__init__.py index 5c972fe660..447fdb2267 100644 --- a/arcade/gui/experimental/__init__.py +++ b/arcade/gui/experimental/__init__.py @@ -1,5 +1,4 @@ -""" -Contains experimental GUI elements. +"""Contains experimental GUI elements. The API of these components may change within minor version updates. No Deprecation warnings are given for changes in this module. diff --git a/arcade/gui/experimental/password_input.py b/arcade/gui/experimental/password_input.py index 0fa67e9e22..d417488a1c 100644 --- a/arcade/gui/experimental/password_input.py +++ b/arcade/gui/experimental/password_input.py @@ -9,11 +9,13 @@ class UIPasswordInput(UIInputText): """A password input field. The text is hidden with asterisks.""" def on_event(self, event: UIEvent) -> Optional[bool]: + """Remove new lines from the input, which are not allowed in passwords.""" if isinstance(event, UITextInputEvent): - event.text = event.text.replace("\n", "").replace("\r", "") # remove new lines! + event.text = event.text.replace("\n", "").replace("\r", "") return super().on_event(event) def do_render(self, surface: Surface): + """Override to render the text as asterisks.""" self.layout.begin_update() position = self.caret.position text = self.text diff --git a/arcade/gui/experimental/scroll_area.py b/arcade/gui/experimental/scroll_area.py index 9c029c62e1..81d50c2d9f 100644 --- a/arcade/gui/experimental/scroll_area.py +++ b/arcade/gui/experimental/scroll_area.py @@ -19,7 +19,22 @@ class UIScrollArea(UIWidget): - """A widget that can scroll its children.""" + """A widget that can scroll its children. + + This widget is highly experimental and only provides a proof of concept. + + Args: + x: x position of the widget + y: y position of the widget + width: width of the widget + height: height of the widget + children: children of the widget + size_hint: size hint of the widget + size_hint_min: minimum size hint of the widget + size_hint_max: maximum size hint of the widget + canvas_size: size of the canvas, which is scrollable + **kwargs: passed to UIWidget + """ scroll_x = Property[float](default=0.0) scroll_y = Property[float](default=0.0) @@ -60,6 +75,7 @@ def __init__( bind(self, "scroll_y", self.trigger_full_render) def remove(self, child: "UIWidget"): + """Remove a child from the widget.""" super().remove(child) self.trigger_full_render() @@ -87,6 +103,7 @@ def _do_render(self, surface: Surface, force=False) -> bool: return rendered def do_render(self, surface: Surface): + """Renders the scolled surface into the given surface.""" self.prepare_render(surface) # draw the whole surface, the scissor box, will limit the visible area on screen width, height = self.surface.size @@ -94,6 +111,7 @@ def do_render(self, surface: Surface): self.surface.draw(LBWH(0, 0, width, height)) def on_event(self, event: UIEvent) -> Optional[bool]: + """Handle scrolling of the widget.""" if isinstance(event, UIMouseDragEvent) and not self.rect.point_in_rect(event.pos): return EVENT_UNHANDLED diff --git a/arcade/gui/mixins.py b/arcade/gui/mixins.py index a8a52b5ee7..f8c6667361 100644 --- a/arcade/gui/mixins.py +++ b/arcade/gui/mixins.py @@ -3,14 +3,14 @@ from typing import Optional from pyglet.event import EVENT_HANDLED, EVENT_UNHANDLED +from typing_extensions import override from arcade.gui.events import UIMouseDragEvent, UIMouseEvent from arcade.gui.widgets import UILayout, UIWidget class UIDraggableMixin(UILayout): - """ - UIDraggableMixin can be used to make any :class:`UIWidget` draggable. + """UIDraggableMixin can be used to make any :class:`UIWidget` draggable. Example, create a draggable Frame, with a background, useful for window like constructs: @@ -19,16 +19,33 @@ class DraggablePane(UITexturePane, UIDraggableMixin): This does overwrite :class:`UILayout` behavior which position themselves, like :class:`UIAnchorWidget` + + warning: + + This mixin in its current form is not recommended for production use. + It is a quick way to get a draggable window like widget. + It does not respect the layout system and can break other widgets + which rely on the layout system. + + Further the dragging is not smooth, as it uses a very simple approach. + + Will be fixed in future versions, but might break existing code within a minor update. + """ + @override def do_layout(self): + # FIXME this breaks all rules, let us not do this + # Preserve top left alignment, this overwrites self placing behavior like # from :class:`UIAnchorWidget` rect = self.rect super().do_layout() self.rect = self.rect.align_top(rect.top).align_left(rect.left) + @override def on_event(self, event) -> Optional[bool]: + """Handle dragging of the widget.""" if isinstance(event, UIMouseDragEvent) and self.rect.point_in_rect(event.pos): self.rect = self.rect.move(event.dx, event.dy) self.trigger_full_render() @@ -40,15 +57,16 @@ def on_event(self, event) -> Optional[bool]: class UIMouseFilterMixin(UIWidget): - """ - :class:`UIMouseFilterMixin` can be used to catch all mouse events which occur + """:class:`UIMouseFilterMixin` can be used to catch all mouse events which occur inside this widget. Useful for window like widgets, :class:`UIMouseEvents` should not trigger effects which are under the widget. """ + @override def on_event(self, event) -> Optional[bool]: + """Catch all mouse events, that are inside this widget.""" if super().on_event(event): return EVENT_HANDLED @@ -61,8 +79,7 @@ def on_event(self, event) -> Optional[bool]: class UIWindowLikeMixin(UIMouseFilterMixin, UIDraggableMixin, UIWidget): - """ - Makes a widget window like: + """Makes a widget window like: - handles all mouse events that occur within the widgets boundaries - can be dragged diff --git a/arcade/gui/nine_patch.py b/arcade/gui/nine_patch.py index 847ccc8743..1bf1e5d204 100644 --- a/arcade/gui/nine_patch.py +++ b/arcade/gui/nine_patch.py @@ -7,8 +7,7 @@ class NinePatchTexture: - """ - Keeps borders & corners at constant widths while stretching the middle. + """Keeps borders & corners at constant widths while stretching the middle. It can be used with new or existing :py:class:`~arcade.gui.UIWidget` subclasses wherever an ordinary :py:class:`arcade.Texture` is @@ -54,17 +53,15 @@ class NinePatchTexture: * Areas ``(2)`` and ``(8)`` only stretch horizontally. * Areas ``(4)`` and ``(6)`` only stretch vertically. - :param left: The width of the left border of the 9-patch - (in pixels) - :param right: The width of the right border of the 9-patch - (in pixels) - :param bottom: The height of the bottom border of the 9-patch - (in pixels) - :param top: The height of the top border of the 9-patch - (in pixels) - :param texture: The raw texture to use for the 9-patch - :param atlas: Specify an atlas other than arcade's default - texture atlas + Args: + left: The width of the left border of the 9-patch (in pixels) + right: The width of the right border of the 9-patch (in pixels) + bottom: The height of the bottom border of the 9-patch (in + pixels) + top: The height of the top border of the 9-patch (in pixels) + texture: The raw texture to use for the 9-patch + atlas: Specify an atlas other than arcade's default texture + atlas """ def __init__( @@ -122,8 +119,7 @@ def texture(self, texture: arcade.Texture): @property def program(self) -> gl.program.Program: - """ - Get or set the shader program. + """Get or set the shader program. Returns the default shader if no other shader is assigned. """ @@ -134,8 +130,7 @@ def program(self, program: gl.program.Program): self._program = program def _add_to_atlas(self, texture: arcade.Texture): - """ - Internal method for setting the texture. + """Internal method for setting the texture. It ensures the texture is added to the global atlas. """ @@ -201,17 +196,18 @@ def draw_rect( blend: bool = True, **kwargs, ): - """ - Draw the 9-patch texture with a specific size. - - .. warning:: This method assumes the passed dimensions are proper! + """Draw the 9-patch texture with a specific size. - Unexpected behavior may occur if you specify a size - smaller than the total size of the border areas. + Warning: + This method assumes the passed dimensions are proper! + Unexpected behavior may occur if you specify a size + smaller than the total size of the border areas. - :param rect: Rectangle to draw the 9-patch texture in - :param pixelated: Whether to draw with nearest neighbor interpolation + Args: + rect: Rectangle to draw the 9-patch texture in + pixelated: Whether to draw with nearest neighbor + interpolation """ if blend: self._ctx.enable_only(self._ctx.BLEND) diff --git a/arcade/gui/property.py b/arcade/gui/property.py index 93b4f9a4a3..10facc1b0a 100644 --- a/arcade/gui/property.py +++ b/arcade/gui/property.py @@ -5,6 +5,8 @@ from typing import Any, Callable, Generic, Optional, TypeVar, cast from weakref import WeakKeyDictionary, ref +from typing_extensions import override + P = TypeVar("P") @@ -23,8 +25,7 @@ def __init__(self, value: P): class Property(Generic[P]): - """ - An observable property which triggers observers when changed. + """An observable property which triggers observers when changed. .. code-block:: python @@ -41,13 +42,19 @@ class MyObject: my_obj.name = "Hans" # > Something changed - :param default: Default value which is returned, if no value set before - :param default_factory: A callable which returns the default value. - Will be called with the property and the instance + Args: + default: Default value which is returned, if no value set before + default_factory: A callable which returns the default value. + Will be called with the property and the instance """ __slots__ = ("name", "default_factory", "obs") name: str + """Attribute name of the property""" + default_factory: Callable[[Any, Any], P] + """Default factory to create the initial value""" + obs: WeakKeyDictionary[Any, _Obs] + """Weak dictionary to hold the value and listeners""" def __init__( self, @@ -68,16 +75,25 @@ def _get_obs(self, instance) -> _Obs: return obs def get(self, instance) -> P: + """Get value for owner instance""" obs = self._get_obs(instance) return obs.value def set(self, instance, value): + """Set value for owner instance""" obs = self._get_obs(instance) if obs.value != value: obs.value = value self.dispatch(instance, value) def dispatch(self, instance, value): + """Notifies every listener, which subscribed to the change event. + + Args: + instance: Property instance + value: new value to set + + """ obs = self._get_obs(instance) for listener in obs.listeners: try: @@ -96,6 +112,14 @@ def dispatch(self, instance, value): traceback.print_exc() def bind(self, instance, callback): + """Binds a function to the change event of the property. + + A reference to the function will be kept. + + Args: + instance: The instance to bind the callback to. + callback: The callback to bind. + """ obs = self._get_obs(instance) # Instance methods are bound methods, which can not be referenced by normal `ref()` # if listeners would be a WeakSet, we would have to add listeners as WeakMethod @@ -103,6 +127,12 @@ def bind(self, instance, callback): obs.listeners.add(callback) def unbind(self, instance, callback): + """Unbinds a function from the change event of the property. + + Args: + instance: The target instance. + callback: The callback to unbind. + """ obs = self._get_obs(instance) obs.listeners.remove(callback) @@ -119,9 +149,12 @@ def __set__(self, instance, value): def bind(instance, property: str, callback): - """ - Binds a function to the change event of the property. A reference to the function will be kept, - so that it will be still invoked, even if it would normally have been garbage collected. + """Bind a function to the change event of the property. + + A reference to the function will be kept, so that it will be still + invoked even if it would normally have been garbage collected: + + .. code-block:: python def log_change(instance, value): print(f"Value of {instance} changed to {value}") @@ -135,10 +168,13 @@ class MyObject: my_obj.name = "Hans" # > Value of <__main__.MyObject ...> changed to Hans - :param instance: Instance owning the property - :param property: Name of the property - :param callback: Function to call - :return: None + Args: + instance: Instance owning the property + property: Name of the property + callback: Function to call + + Returns: + None """ t = type(instance) prop = getattr(t, property) @@ -147,8 +183,9 @@ class MyObject: def unbind(instance, property: str, callback): - """ - Unbinds a function from the change event of the property. + """Unbinds a function from the change event of the property. + + .. code-block:: python def log_change(instance, value): print("Something changed") @@ -163,11 +200,13 @@ class MyObject: my_obj.name = "Hans" # > Something changed + Args: + instance: Instance owning the property + property: Name of the property + callback: Function to unbind - :param instance: Instance owning the property - :param property: Name of the property - :param callback: Function to unbind - :return: None + Returns: + None """ t = type(instance) prop = getattr(t, property) @@ -180,133 +219,171 @@ class _ObservableDict(dict): __slots__ = ("prop", "obj") - def __init__(self, prop: Property, instance, *largs): + def __init__(self, prop: Property, instance, *args): self.prop: Property = prop self.obj = ref(instance) - super().__init__(*largs) + super().__init__(*args) def dispatch(self): self.prop.dispatch(self.obj(), self) + @override def __setitem__(self, key, value): dict.__setitem__(self, key, value) self.dispatch() + @override def __delitem__(self, key): dict.__delitem__(self, key) self.dispatch() + @override def clear(self): dict.clear(self) self.dispatch() - def pop(self, *largs): - result = dict.pop(self, *largs) + @override + def pop(self, *args): + result = dict.pop(self, *args) self.dispatch() return result + @override def popitem(self): result = dict.popitem(self) self.dispatch() return result - def setdefault(self, *largs): - dict.setdefault(self, *largs) + @override + def setdefault(self, *args): + dict.setdefault(self, *args) self.dispatch() - def update(self, *largs): - dict.update(self, *largs) + @override + def update(self, *args): + dict.update(self, *args) self.dispatch() class DictProperty(Property): - """ - Property that represents a dict. + """Property that represents a dict. + Only dict are allowed. Any other classes are forbidden. """ def __init__(self): super().__init__(default_factory=_ObservableDict) + @override def set(self, instance, value: dict): + """Set value for owner instance, wraps the dict into an observable dict.""" value = _ObservableDict(self, instance, value) super().set(instance, value) class _ObservableList(list): - """Internal class to observe changes inside a native python list.""" + """Internal class to observe changes inside a native python list. + + Args: + prop: Property instance + instance: Instance owning the property + *args: List of arguments to pass to the list + """ __slots__ = ("prop", "obj") - def __init__(self, prop: Property, instance, *largs): + def __init__(self, prop: Property, instance, *args): self.prop: Property = prop self.obj = ref(instance) - super().__init__(*largs) + super().__init__(*args) def dispatch(self): + """Dispatches the change event.""" self.prop.dispatch(self.obj(), self) + @override def __setitem__(self, key, value): list.__setitem__(self, key, value) self.dispatch() + @override def __delitem__(self, key): list.__delitem__(self, key) self.dispatch() - def __iadd__(self, *largs): # type: ignore - list.__iadd__(self, *largs) + @override + def __iadd__(self, *args): # type: ignore + list.__iadd__(self, *args) self.dispatch() return self - def __imul__(self, *largs): # type: ignore - list.__imul__(self, *largs) + @override + def __imul__(self, *args): # type: ignore + list.__imul__(self, *args) self.dispatch() return self - def append(self, *largs): - list.append(self, *largs) + @override + def append(self, *args): + """Proxy for list.append() which dispatches the change event.""" + list.append(self, *args) self.dispatch() + @override def clear(self): + """Proxy for list.clear() which dispatches the change event.""" list.clear(self) self.dispatch() - def remove(self, *largs): - list.remove(self, *largs) + @override + def remove(self, *args): + """Proxy for list.remove() which dispatches the change event.""" + list.remove(self, *args) self.dispatch() - def insert(self, *largs): - list.insert(self, *largs) + @override + def insert(self, *args): + """Proxy for list.insert() which dispatches the change event.""" + list.insert(self, *args) self.dispatch() - def pop(self, *largs): - result = list.pop(self, *largs) + @override + def pop(self, *args): + """Proxy for list.pop() which dispatches the change""" + result = list.pop(self, *args) self.dispatch() return result - def extend(self, *largs): - list.extend(self, *largs) + @override + def extend(self, *args): + """Proxy for list.extend() which dispatches the change event.""" + list.extend(self, *args) self.dispatch() + @override def sort(self, **kwargs): + """Proxy for list.sort() which dispatches the change event.""" list.sort(self, **kwargs) self.dispatch() + @override def reverse(self): + """Proxy for list.reverse() which dispatches the change event.""" list.reverse(self) self.dispatch() class ListProperty(Property): - """ - Property that represents a list. + """Property that represents a list. + Only list are allowed. Any other classes are forbidden. """ def __init__(self): super().__init__(default_factory=_ObservableList) + @override def set(self, instance, value: dict): + """Set value for owner instance, wraps the list into an observable list.""" value = _ObservableList(self, instance, value) # type: ignore super().set(instance, value) diff --git a/arcade/gui/style.py b/arcade/gui/style.py index 72b77fc080..67ed8a54e5 100644 --- a/arcade/gui/style.py +++ b/arcade/gui/style.py @@ -10,8 +10,7 @@ @dataclass class UIStyleBase: - """ - Base class for styles to ensure a general interface and implement additional magic. + """Base class for styles to ensure a general interface and implement additional magic. Support dict like access syntax. @@ -25,6 +24,7 @@ def get(self, key, default: str) -> str: ... def get(self, key, default: Any) -> Any: ... def get(self, key, default=None): + """Get a value from the style, if not available return default.""" return self[key] if key in self else default def __contains__(self, item): @@ -41,6 +41,20 @@ def __setitem__(self, key, value): class UIStyledWidget(UIWidget, Generic[StyleRef]): + """Baseclass for widgets to be styled. + + A styled widget should own a dataclass, which subclasses UIStyleBase. + The style dict should contain the different states of the widget. + + The widget should implement py:method::`get_current_state()`, + which should return the current state of the widget. + + Args: + style: A mapping of states to styles + **kwargs: passed to UIWidget + + """ + # TODO detect StyleBase changes, so that style changes can trigger rendering. style: Mapping = DictProperty() # type: ignore diff --git a/arcade/gui/surface.py b/arcade/gui/surface.py index 16ac38d17b..1f330b428b 100644 --- a/arcade/gui/surface.py +++ b/arcade/gui/surface.py @@ -1,7 +1,9 @@ from __future__ import annotations from contextlib import contextmanager -from typing import Optional +from typing import Generator, Optional + +from typing_extensions import Self import arcade from arcade import Texture @@ -13,9 +15,15 @@ class Surface: - """ - Holds a :class:`arcade.gl.Framebuffer` and abstracts the drawing on it. - Used internally for rendering widgets. + """Internal abstraction for widget rendering. + + Holds a :class:`arcade.gl.Framebuffer` and provides helper methods + and properties for drawing to it. + + Args: + size: The size of the surface in window coordinates + position: The position of the surface in window + pixel_ratio: The pixel scale of the window """ def __init__( @@ -82,14 +90,17 @@ def size_scaled(self): @property def pixel_ratio(self) -> float: + """The pixel ratio of the surface""" return self._pixel_ratio @property def width(self) -> int: + """Width of the surface""" return self._size[0] @property def height(self) -> int: + """Height of the surface""" return self._size[1] def clear(self, color: RGBA255 = TRANSPARENT_BLACK): @@ -106,6 +117,17 @@ def draw_texture( angle: float = 0.0, alpha: int = 255, ): + """Draw a texture to the surface. + + Args: + x: The x coordinate of the texture. + y: The y coordinate of the texture. + width: The width of the texture. + height: The height of the texture. + tex: The texture to draw, also supports NinePatchTexture. + angle: The angle of the texture. + alpha: The alpha value of the texture. + """ if isinstance(tex, NinePatchTexture): if angle != 0.0: raise NotImplementedError( @@ -122,17 +144,37 @@ def draw_texture( arcade.draw_texture_rect(tex, LBWH(x, y, width, height), angle=angle, alpha=alpha) def draw_sprite(self, x: float, y: float, width: float, height: float, sprite: arcade.Sprite): - """Draw a sprite to the surface""" + """Draw a sprite to the surface + + Args: + x: The x coordinate of the sprite. + y: The y coordinate of the sprite. + width: The width of the sprite. + height: The height of the sprite. + sprite: The sprite to draw. + """ sprite.position = x + width // 2, y + height // 2 sprite.width = width sprite.height = height arcade.draw_sprite(sprite) @contextmanager - def activate(self): - """ - Save and restore projection and activate Surface buffer to draw on. - Also resets the limit of the surface (viewport). + def activate(self) -> Generator[Self, None, None]: + """Context manager for rendering safely to this :py:class:`Surface`. + + It does the following: + + #. Apply this surface's viewport, projection, and blend settings + #. Allow any rendering to take place + #. Restore the old OpenGL context settings + + Use it in ``with`` blocks like other managers: + + .. code-block:: python + + with surface.activate(): + # draw stuff here + """ # Set viewport and projection self.limit(LBWH(0, 0, *self.size)) @@ -147,7 +189,7 @@ def activate(self): # Restore blend function. self.ctx.blend_func = prev_blend_func - def limit(self, rect: Rect): # TODO track limit usage + def limit(self, rect: Rect): """Reduces the draw area to the given rect""" l, b, w, h = rect.lbwh @@ -171,13 +213,14 @@ def draw( self, area: Optional[Rect] = None, ) -> None: - """ - Draws the contents of the surface. + """Draws the contents of the surface. The surface will be rendered at the configured ``position`` and limited by the given ``area``. The area can be out of bounds. - :param area: Limit the area in the surface we're drawing (l, b, w, h) + Args: + area: Limit the area in the surface we're drawing + (l, b, w, h) """ # Set blend function blend_func = self.ctx.blend_func @@ -193,11 +236,11 @@ def draw( self.ctx.blend_func = blend_func def resize(self, *, size: tuple[int, int], pixel_ratio: float) -> None: - """ - Resize the internal texture by re-allocating a new one + """Resize the internal texture by re-allocating a new one - :param size: The new size in pixels (xy) - :param pixel_ratio: The pixel scale of the window + Args: + size: The new size in pixels (xy) + pixel_ratio: The pixel scale of the window """ # Texture re-allocation is expensive so we should block unnecessary calls. if self._size == size and self._pixel_ratio == pixel_ratio: diff --git a/arcade/gui/ui_manager.py b/arcade/gui/ui_manager.py index 5650e1e62e..821cab46eb 100644 --- a/arcade/gui/ui_manager.py +++ b/arcade/gui/ui_manager.py @@ -1,5 +1,4 @@ -""" -The better gui for arcade +"""The better gui for arcade - Improved events, now fully typed - UIElements are now called Widgets (like everywhere else) @@ -18,6 +17,7 @@ from typing_extensions import TypeGuard import arcade +from arcade.gui import UIEvent from arcade.gui.events import ( UIKeyPressEvent, UIKeyReleaseEvent, @@ -82,6 +82,8 @@ def on_draw(): manager.draw() # draws the UI on screen + Args: + window: The window to bind the UIManager to, if None the current window is used. """ _enabled = False @@ -102,21 +104,22 @@ def __init__(self, window: Optional[arcade.Window] = None): self.register_event_type("on_event") # type: ignore # https://github.com/pyglet/pyglet/pull/1173 # noqa def add(self, widget: W, *, index=None, layer=0) -> W: - """ - Add a widget to the :class:`UIManager`. + """Add a widget to the :class:`UIManager`. + Added widgets will receive ui events and be rendered. - By default the latest added widget will receive ui events first and will + By default, the latest added widget will receive ui events first and will be rendered on top of others. The UIManager supports layered setups, widgets added to a higher layer are drawn above lower layers and receive events first. The layer 10 is reserved for overlaying components like dropdowns or tooltips. - :param widget: widget to add - :param index: position a widget is added, None has the highest priority - :param layer: layer which the widget should be added to, higher layer are above - :return: the widget + Args: + widget: widget to add + index: position a widget is added, None has the highest priority + layer: layer which the widget should be added to, higher layer are above + """ if index is None: self.children[layer].append(widget) @@ -127,10 +130,10 @@ def add(self, widget: W, *, index=None, layer=0) -> W: return widget def remove(self, child: UIWidget): - """ - Removes the given widget from UIManager. + """Removes the given widget from UIManager. - :param child: widget to remove + Args: + child: widget to remove """ for children in self.children.values(): if child in children: @@ -139,11 +142,11 @@ def remove(self, child: UIWidget): self.trigger_render() def walk_widgets(self, *, root: Optional[UIWidget] = None, layer=0) -> Iterable[UIWidget]: - """ - walks through widget tree, in reverse draw order (most top drawn widget first) + """Walks through widget tree, in reverse draw order (most top drawn widget first) - :param root: root widget to start from, if None, the layer is used - :param layer: layer to search, None will search through all layers + Args: + root: root widget to start from, if None, the layer is used + layer: layer to search, None will search through all layers """ if layer is None: layers = sorted(self.children.keys(), reverse=True) @@ -157,22 +160,21 @@ def walk_widgets(self, *, root: Optional[UIWidget] = None, layer=0) -> Iterable[ yield child def clear(self): - """ - Remove all widgets from UIManager - """ + """Remove all widgets from UIManager""" for layer in self.children.values(): for widget in layer[:]: self.remove(widget) def get_widgets_at(self, pos: Point2, cls: type[W] = UIWidget, layer=0) -> Iterable[W]: - """ - Yields all widgets containing a position, returns first top laying widgets + """Yields all widgets containing a position, returns first top laying widgets which is instance of cls. - :param pos: Pos within the widget bounds - :param cls: class which the widget should be an instance of - :param layer: layer to search, None will search through all layers - :return: iterator of widgets of given type at position + Args: + pos: Pos within the widget bounds + cls: class which the widget should be an instance of + layer: layer to search, None will search through all layers + + Returns: iterator of widgets of given type at position """ def check_type(widget) -> TypeGuard[W]: @@ -195,14 +197,11 @@ def _get_surface(self, layer: int) -> Surface: return self._surfaces[layer] def trigger_render(self): - """ - Request rendering of all widgets before next draw - """ + """Request rendering of all widgets before next draw""" self._requires_render = True def execute_layout(self): - """ - Execute layout process for all widgets. + """Execute layout process for all widgets. This is automatically called during :py:meth:`UIManager.draw()`. """ @@ -261,8 +260,7 @@ def _do_render(self, force=False): self._requires_render = False def enable(self) -> None: - """ - Registers handler functions (`on_...`) to :py:attr:`arcade.gui.UIElement` + """Registers handler functions (`on_...`) to :py:attr:`arcade.gui.UIElement` on_draw is not registered, to provide full control about draw order, so it has to be called by the devs themselves. @@ -287,8 +285,7 @@ def enable(self) -> None: ) def disable(self) -> None: - """ - Remove handler functions (`on_...`) from :py:attr:`arcade.Window` + """Remove handler functions (`on_...`) from :py:attr:`arcade.Window` If every :py:class:`arcade.View` uses its own :py:class:`arcade.gui.UIManager`, this method should be called in :py:meth:`arcade.View.on_hide_view()`. @@ -311,11 +308,11 @@ def disable(self) -> None: ) def on_update(self, time_delta): + """Dispatches an update event to all widgets in the UIManager.""" return self.dispatch_ui_event(UIOnUpdateEvent(self, time_delta)) def draw(self) -> None: - """ - Will draw all widgets to the window. + """Will draw all widgets to the window. UIManager caches all rendered widgets into a framebuffer (something like a window sized image) and only updates the framebuffer if a widget requests @@ -345,8 +342,7 @@ def draw(self) -> None: self._get_surface(layer).draw() def adjust_mouse_coordinates(self, x: float, y: float) -> tuple[float, float]: - """ - This method is used, to translate mouse coordinates to coordinates + """This method is used, to translate mouse coordinates to coordinates respecting the viewport and projection of cameras. It uses the internal camera's map_coordinate methods, and should work with @@ -356,6 +352,7 @@ def adjust_mouse_coordinates(self, x: float, y: float) -> tuple[float, float]: return x_, y_ def on_event(self, event) -> Union[bool, None]: + """Forwards an event to all widgets in the UIManager.""" layers = sorted(self.children.keys(), reverse=True) for layer in layers: for child in reversed(self.children[layer]): @@ -364,53 +361,70 @@ def on_event(self, event) -> Union[bool, None]: return EVENT_HANDLED return EVENT_UNHANDLED - def dispatch_ui_event(self, event): + def dispatch_ui_event(self, event: UIEvent): + """Dispatch a UI event to all widgets in the UIManager, + by triggering py:meth:`on_event()`. + + Args: + event: The event to dispatch. + """ return self.dispatch_event("on_event", event) def on_mouse_motion(self, x: int, y: int, dx: int, dy: int): + """Converts mouse motion event to UI event and dispatches it.""" x_, y_ = self.adjust_mouse_coordinates(x, y) return self.dispatch_ui_event(UIMouseMovementEvent(self, round(x_), round(y_), dx, dy)) def on_mouse_press(self, x: int, y: int, button: int, modifiers: int): + """Converts mouse press event to UI event and dispatches it.""" x_, y_ = self.adjust_mouse_coordinates(x, y) return self.dispatch_ui_event( UIMousePressEvent(self, round(x_), round(y_), button, modifiers) ) def on_mouse_drag(self, x: int, y: int, dx: int, dy: int, buttons: int, modifiers: int): + """Converts mouse drag event to UI event and dispatches it.""" x_, y_ = self.adjust_mouse_coordinates(x, y) return self.dispatch_ui_event( UIMouseDragEvent(self, round(x_), round(y_), dx, dy, buttons, modifiers) ) def on_mouse_release(self, x: int, y: int, button: int, modifiers: int): + """Converts mouse release event to UI event and dispatches it.""" x_, y_ = self.adjust_mouse_coordinates(x, y) return self.dispatch_ui_event( UIMouseReleaseEvent(self, round(x_), round(y_), button, modifiers) ) def on_mouse_scroll(self, x, y, scroll_x, scroll_y): + """Converts mouse scroll event to UI event and dispatches it.""" x_, y_ = self.adjust_mouse_coordinates(x, y) return self.dispatch_ui_event( UIMouseScrollEvent(self, round(x_), round(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 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 def on_text(self, text): + """Converts text event to UI event and dispatches it.""" return self.dispatch_ui_event(UITextInputEvent(self, text)) def on_text_motion(self, motion): + """Converts text motion event to UI event and dispatches it.""" return self.dispatch_ui_event(UITextMotionEvent(self, motion)) def on_text_motion_select(self, motion): + """Converts text motion select event to UI event and dispatches it.""" return self.dispatch_ui_event(UITextMotionSelectEvent(self, motion)) def on_resize(self, width, height): + """Resize the UIManager and all of its surfaces.""" # resize ui camera bottom_left = self.camera.bottom_left self.camera.match_screen() @@ -429,6 +443,7 @@ def on_resize(self, width, height): @property def rect(self) -> Rect: # type: ignore + """The rect of the UIManager, which is the window size.""" return LBWH(0, 0, *self.window.get_size()) def debug(self): diff --git a/arcade/gui/view.py b/arcade/gui/view.py index 43d3cbf0fe..a93c4e3d85 100644 --- a/arcade/gui/view.py +++ b/arcade/gui/view.py @@ -29,8 +29,7 @@ def on_hide_view(self): self.ui.disable() def on_draw(self): - """ - To subclass UIView and add custom drawing, override on_draw_before_ui + """To subclass UIView and add custom drawing, override on_draw_before_ui and on_draw_after_ui. """ self.clear() diff --git a/arcade/gui/widgets/__init__.py b/arcade/gui/widgets/__init__.py index 495ac3d31f..6d4abb5fdf 100644 --- a/arcade/gui/widgets/__init__.py +++ b/arcade/gui/widgets/__init__.py @@ -40,8 +40,7 @@ class _ChildEntry(NamedTuple): @copy_dunders_unimplemented class UIWidget(EventDispatcher, ABC): - """ - The :class:`UIWidget` class is the base class required for creating widgets. + """The :class:`UIWidget` class is the base class required for creating widgets. We also have some default values and behaviors that you should be aware of: @@ -49,14 +48,14 @@ class UIWidget(EventDispatcher, ABC): change the position or the size of its children. If you want control over positioning or sizing, use a :class:`~arcade.gui.UILayout`. - :param x: x coordinate of bottom left - :param y: y coordinate of bottom left - :param width: width of widget - :param height: height of widget - :param size_hint: Tuple of floats (0.0-1.0), how much space of the parent should be requested - :param size_hint_min: min width and height in pixel - :param size_hint_max: max width and height in pixel - :param style: not used + Args: + x: x coordinate of bottom left + y: y coordinate of bottom left + width: width of widget + height: height of widget + size_hint: Tuple of floats (0.0-1.0), how much space of the parent should be requested + size_hint_min: min width and height in pixel + size_hint_max: max width and height in pixel """ rect: Rect = Property(LBWH(0, 0, 1, 1)) # type: ignore @@ -120,16 +119,20 @@ def __init__( bind(self, "_padding_left", self.trigger_render) def add(self, child: W, **kwargs) -> W: - """ - Add a widget to this :class:`UIWidget` as a child. - Added widgets will receive ui events and be rendered. + """Add a widget as a child. + + Added widgets will receive UI events and be rendered. By default, the latest added widget will receive ui events first and will be rendered on top of others. - :param child: widget to add - :param index: position a widget is added, None has the highest priority - :return: given child + Args: + child: widget to add + index: position a widget is added, None has the highest + priority + + Returns: + given child """ child.parent = self index = kwargs.pop("index") if "index" in kwargs else None @@ -143,8 +146,7 @@ def add(self, child: W, **kwargs) -> W: return child def remove(self, child: "UIWidget"): - """ - Removes a child from the UIManager which was directly added to it. + """Removes a child from the UIManager which was directly added to it. This will not remove widgets which are added to a child of UIManager. """ child.parent = None @@ -153,6 +155,7 @@ def remove(self, child: "UIWidget"): self._children.remove(c) def clear(self): + """Removes all children""" for child in self.children: child.parent = None @@ -189,15 +192,15 @@ def _walk_parents(self) -> Iterable[Union["UIWidget", "UIManager"]]: yield parent # type: ignore def trigger_render(self): - """ - This will delay a render right before the next frame is rendered, so that + """This will delay a render right before the next frame is rendered, so that :meth:`UIWidget.do_render` is not called multiple times. """ self._requires_render = True def trigger_full_render(self) -> None: """In case a widget uses transparent areas or was moved, - it might be important to request a full rendering of parents""" + it might be important to request a full rendering of parents + """ self.trigger_render() for parent in self._walk_parents(): parent.trigger_render() @@ -221,7 +224,8 @@ def _do_render(self, surface: Surface, force=False) -> bool: """Helper function to trigger :meth:`UIWidget.do_render` through the widget tree, should only be used by UIManager! - :return: if this widget or a child was rendered + Returns: + if this widget or a child was rendered """ rendered = False @@ -240,9 +244,7 @@ def _do_render(self, surface: Surface, force=False) -> bool: return rendered def do_render_base(self, surface: Surface): - """ - Renders background, border and "padding" - """ + """Renders background, border and "padding""" surface.limit(self.rect) # draw background @@ -264,12 +266,12 @@ def do_render_base(self, surface: Surface): ) def prepare_render(self, surface): - """ - Helper for rendering, the drawing area will be adjusted to the own position and size. + """Helper for rendering, the drawing area will be adjusted to the own position and size. Draw calls have to be relative to 0,0. This will also prevent any overdraw outside of the widgets area - :param surface: Surface used for rendering + Args: + surface: Surface used for rendering """ surface.limit(self.content_rect) @@ -284,45 +286,51 @@ def dispatch_ui_event(self, event: UIEvent): return self.dispatch_event("on_event", event) def move(self, dx=0, dy=0): - """ - Move the widget by dx and dy. + """Move the widget by dx and dy. - :param dx: x axis difference - :param dy: y axis difference + Args: + dx: x axis difference + dy: y axis difference """ self.rect = self.rect.move(dx, dy) def scale(self, factor: AsFloat, anchor: Vec2 = AnchorPoint.CENTER): - """ - Scales the size of the widget (x,y,width, height) by factor. - :param factor: scale factor - :param anchor: anchor point + """Scales the size of the widget (x,y,width, height) by factor. + + Args: + factor: scale factor + anchor: anchor point """ self.rect = self.rect.scale(new_scale=factor, anchor=anchor) @property - def left(self): + def left(self) -> float: + """Left coordinate of the widget""" return self.rect.left @property - def right(self): + def right(self) -> float: + """Right coordinate of the widget""" return self.rect.right @property - def bottom(self): + def bottom(self) -> float: + """Bottom coordinate of the widget""" return self.rect.bottom @property - def top(self): + def top(self) -> float: + """Top coordinate of the widget""" return self.rect.top @property - def position(self): + def position(self) -> Vec2: """Returns bottom left coordinates""" return self.rect.bottom_left @property - def center(self): + def center(self) -> Vec2: + """Returns center coordinates""" return self.rect.center @center.setter @@ -330,15 +338,18 @@ def center(self, value: Tuple[int, int]): self.rect = self.rect.align_center(value) @property - def center_x(self): + def center_x(self) -> float: + """Center x coordinate""" return self.rect.x @property - def center_y(self): + def center_y(self) -> float: + """Center y coordinate""" return self.rect.y @property def padding(self): + """Returns padding as tuple (top, right, bottom, left)""" return ( self._padding_top, self._padding_right, @@ -362,20 +373,31 @@ def padding(self, args: Union[int, Tuple[int, int], Tuple[int, int, int, int]]): @property def children(self) -> List["UIWidget"]: + """Provides all child widgets.""" return [child for child, data in self._children] def __iter__(self): return iter(self.children) def resize(self, *, width=None, height=None, anchor: Vec2 = AnchorPoint.CENTER): - self.rect = self.rect.resize(width=width, height=height, anchor=anchor) + """Resizes the widget. - def with_border(self, *, width=2, color=(0, 0, 0)) -> Self: + Args: + width (optional): new width + height (optional): new height + anchor (optional): anchor point for resizing, default is center """ - Sets border properties - :param width: border width - :param color: border color - :return: self + self.rect = self.rect.resize(width=width, height=height, anchor=anchor) + + def with_border(self, *, width=2, color=arcade.color.GRAY) -> Self: + """Sets border properties + + Args: + width: border width + color: border color + + Returns: + self """ self._border_width = width self._border_color = color @@ -389,10 +411,11 @@ def with_padding( bottom: Optional[int] = None, left: Optional[int] = None, all: Optional[int] = None, - ) -> "UIWidget": - """ - Changes the padding to the given values if set. Returns itself - :return: self + ) -> Self: + """Changes the padding to the given values if set. Returns itself + + Returns: + self """ if all is not None: self.padding = all @@ -411,16 +434,18 @@ def with_background( *, color: Union[None, Color] = ..., # type: ignore texture: Union[None, Texture, NinePatchTexture] = ..., # type: ignore - ) -> "UIWidget": - """ - Set widgets background. + ) -> Self: + """Set widgets background. A color or texture can be used for background, if a texture is given, start and end point can be added to use the texture as ninepatch. - :param color: A color used as background - :param texture: A texture or ninepatch texture used as background - :return: self + Args: + color: A color used as background + texture: A texture or ninepatch texture used as background + + Returns: + self """ if color is not ...: self._bg_color = color @@ -431,19 +456,27 @@ def with_background( return self @property - def content_size(self): + def content_size(self) -> Tuple[float, float]: + """Returns the size of the content area, + which is the size of the widget minus padding and border.""" return self.content_width, self.content_height @property - def content_width(self): + def content_width(self) -> float: + """Returns the width of the content area, + which is the width of the widget minus padding and border.""" return self.rect.width - 2 * self._border_width - self._padding_left - self._padding_right @property - def content_height(self): + def content_height(self) -> float: + """Returns the height of the content area, + which is the height of the widget minus padding and border.""" return self.rect.height - 2 * self._border_width - self._padding_top - self._padding_bottom @property - def content_rect(self): + def content_rect(self) -> Rect: + """Returns the content area as a rect. + The content area is the area of the widget minus padding and border.""" return LBWH( self.left + self._border_width + self._padding_left, self.bottom + self._border_width + self._padding_bottom, @@ -452,20 +485,27 @@ def content_rect(self): ) @property - def width(self): + def width(self) -> float: + """Width of the widget.""" return self.rect.width @property - def height(self): + def height(self) -> float: + """Height of the widget.""" return self.rect.height @property def size(self) -> Vec2: + """Size of the widget.""" return Vec2(self.width, self.height) def center_on_screen(self: W) -> W: - """ - Places this widget in the center of the current window. + """Places this widget in the center of the current window. + + This is a convenience method for simple centering of widgets without using + a layout. + + In general, it is recommended to use layouts for more complex UIs. """ center = arcade.get_window().center self.rect = self.rect.align_center(center) @@ -473,25 +513,30 @@ def center_on_screen(self: W) -> W: class UIInteractiveWidget(UIWidget): - """ - Base class for widgets which use mouse interaction (hover, pressed, clicked) - - :param x: x coordinate of bottom left - :param y: y coordinate of bottom left - :param width: width of widget - :param height: height of widget - :param size_hint: Tuple of floats (0.0-1.0), how much space of the parent should be requested - :param size_hint_min: min width and height in pixel - :param size_hint_max: max width and height in pixel:param x: center x of widget - :param interaction_buttons: defines, which mouse buttons should trigger the - interaction (default: left mouse button) - :param style: not used + """Base class for widgets which use mouse interaction (hover, pressed, clicked) + + Args: + x: x coordinate of bottom left + y: y coordinate of bottom left + width: width of widget + height: height of widget + size_hint: Tuple of floats (0.0-1.0), how much space of the + parent should be requested + size_hint_min: min width and height in pixel + size_hint_max: max width and height in pixel:param x: center x + of widget + interaction_buttons: defines, which mouse buttons should trigger + the interaction (default: left mouse button) + style: not used """ # States hovered = Property(False) + """True if the mouse is over the widget""" pressed = Property(False) + """True if the widget is pressed""" disabled = Property(False) + """True if the widget is disabled""" def __init__( self, @@ -525,6 +570,10 @@ def __init__( bind(self, "disabled", self.trigger_render) def on_event(self, event: UIEvent) -> Optional[bool]: + """Handles mouse events and triggers on_click event if the widget is clicked. + + This also sets the hovered and pressed state of the widget. + """ if super().on_event(event): return EVENT_HANDLED @@ -563,12 +612,12 @@ def on_event(self, event: UIEvent) -> Optional[bool]: return EVENT_UNHANDLED def on_click(self, event: UIOnClickEvent): + """Triggered when the widget is clicked.""" pass class UIDummy(UIInteractiveWidget): - """ - Solid color widget used for testing & examples + """Solid color widget used for testing & examples It should not be subclassed for real-world usage. @@ -577,15 +626,17 @@ class UIDummy(UIInteractiveWidget): * Outputs its `rect` attribute to the console * Changes its color to a random fully opaque color - :param x: x coordinate of bottom left - :param y: y coordinate of bottom left - :param color: fill color for the widget - :param width: width of widget - :param height: height of widget - :param size_hint: Tuple of floats (0.0-1.0), how much space of the parent should be requested - :param size_hint_min: min width and height in pixel - :param size_hint_max: max width and height in pixel - :param style: not used + Args: + x: x coordinate of bottom left + y: y coordinate of bottom left + color: fill color for the widget + width: width of widget + height: height of widget + size_hint: Tuple of floats (0.0-1.0), how much space of the + parent should be requested + size_hint_min: min width and height in pixel + size_hint_max: max width and height in pixel + style: not used """ def __init__( @@ -614,14 +665,17 @@ def __init__( self.border_width = 0 def on_click(self, event: UIOnClickEvent): + """Prints the rect and changes the color""" print("UIDummy.rect:", self.rect) self.color = Color.random(a=255) def on_update(self, dt): + """Update the border of the widget if hovered""" self.border_width = 2 if self.hovered else 0 self.border_color = arcade.color.WHITE if self.pressed else arcade.color.BATTLESHIP_GREY def do_render(self, surface: Surface): + """Render solid color""" self.prepare_render(surface) surface.clear(self.color) @@ -629,15 +683,17 @@ def do_render(self, surface: Surface): class UISpriteWidget(UIWidget): """Create a UI element with a sprite that controls what is displayed. - :param x: x coordinate of bottom left - :param y: y coordinate of bottom left - :param width: width of widget - :param height: height of widget - :param sprite: Sprite to embed in gui - :param size_hint: Tuple of floats (0.0-1.0), how much space of the parent should be requested - :param size_hint_min: min width and height in pixel - :param size_hint_max: max width and height in pixel - :param style: not used + Args: + x: x coordinate of bottom left + y: y coordinate of bottom left + width: width of widget + height: height of widget + sprite: Sprite to embed in gui + size_hint: Tuple of floats (0.0-1.0), how much space of the + parent should be requested + size_hint_min: min width and height in pixel + size_hint_max: max width and height in pixel + style: not used """ def __init__( @@ -666,12 +722,14 @@ def __init__( self._sprite = sprite def on_update(self, dt): + """Pass update event to sprite""" if self._sprite: self._sprite.update() self._sprite.update_animation(dt) self.trigger_render() def do_render(self, surface: Surface): + """Render the sprite""" self.prepare_render(surface) surface.clear(color=TRANSPARENT_BLACK) if self._sprite is not None: @@ -679,25 +737,24 @@ def do_render(self, surface: Surface): class UILayout(UIWidget): - """ - Base class for widgets, which position themselves or their children. - - :param x: x coordinate of bottom left - :param y: y coordinate of bottom left - :param width: width of widget - :param height: height of widget - :param children: Child widgets of this group - :param size_hint: A hint for :class:`UILayout`, if this :class:`UIWidget` would like to grow - :param size_hint: Tuple of floats (0.0-1.0), how much space of the parent should be requested - :param size_hint_min: min width and height in pixel - :param size_hint_max: max width and height in pixel - :param style: not used + """Base class for widgets, which position themselves or their children. + + Args: + x: x coordinate of bottom left + y: y coordinate of bottom left + width: width of widget + height: height of widget + children: Child widgets of this group + size_hint: Tuple of floats (0.0-1.0), how much space of the + parent should be requested + size_hint_min: min width and height in pixel + size_hint_max: max width and height in pixel + style: not used """ @staticmethod def min_size_of(child: UIWidget) -> Tuple[float, float]: - """ - Resolves the minimum size of a child. If it has a size_hint set for the axis, + """Resolves the minimum size of a child. If it has a size_hint set for the axis, it will use size_hint_min if set, otherwise the actual size will be used. """ sh_w, sh_h = child.size_hint or (None, None) @@ -715,13 +772,13 @@ def min_size_of(child: UIWidget) -> Tuple[float, float]: def _prepare_layout(self): """Triggered to prepare layout of this widget and its children. - Common example is to update size_hints(min/max).""" + Common example is to update size_hints(min/max). + """ super()._prepare_layout() self.prepare_layout() def prepare_layout(self): - """ - Triggered by the UIManager before layouting, + """Triggered by the UIManager before layouting, :class:`UILayout` s should prepare themselves based on children. Prepare layout is triggered on children first. @@ -736,8 +793,7 @@ def _do_layout(self): super()._do_layout() def do_layout(self): - """ - Triggered by the UIManager before rendering, :class:`UILayout` s should place + """Triggered by the UIManager before rendering, :class:`UILayout` s should place themselves and/or children. Do layout will be triggered on children afterward. Use :meth:`UIWidget.trigger_render` to trigger a rendering before the next @@ -746,18 +802,19 @@ def do_layout(self): class UISpace(UIWidget): - """ - Widget reserving space, can also have a background color. - - :param x: x coordinate of bottom left - :param y: y coordinate of bottom left - :param width: width of widget - :param height: height of widget - :param color: Color for widget area - :param size_hint: Tuple of floats (0.0-1.0), how much space of the parent should be requested - :param size_hint_min: min width and height in pixel - :param size_hint_max: max width and height in pixel - :param style: not used + """Widget reserving space, can also have a background color. + + Args: + x: x coordinate of bottom left + y: y coordinate of bottom left + width: width of widget + height: height of widget + color: Color for widget area + size_hint: Tuple of floats (0.0-1.0), how much space of the + parent should be requested + size_hint_min: min width and height in pixel + size_hint_max: max width and height in pixel + **kwargs: passed to UIWidget """ def __init__( @@ -781,11 +838,13 @@ def __init__( size_hint=size_hint, size_hint_min=size_hint_min, size_hint_max=size_hint_max, + **kwargs, ) self._color = color @property def color(self): + """Color of the widget""" return self._color @color.setter @@ -794,6 +853,7 @@ def color(self, value): self.trigger_render() def do_render(self, surface: Surface): + """Render the widget, mainly the background color""" self.prepare_render(surface) if self._color: surface.clear(self._color) diff --git a/arcade/gui/widgets/buttons.py b/arcade/gui/widgets/buttons.py index f587633f62..3cd10a5af5 100644 --- a/arcade/gui/widgets/buttons.py +++ b/arcade/gui/widgets/buttons.py @@ -17,8 +17,7 @@ @dataclass class UITextureButtonStyle(UIStyleBase): - """ - Used to style the texture button. Below is its use case. + """Used to style the texture button. Below is its use case. .. code:: py @@ -32,26 +31,30 @@ class UITextureButtonStyle(UIStyleBase): class UITextureButton(UIInteractiveWidget, UIStyledWidget[UITextureButtonStyle], UITextWidget): - """ - A button with an image for the face of the button. - - There are four states of the UITextureButton i.e normal, hovered, pressed and disabled. - - :param x: x coordinate of bottom left - :param y: y coordinate of bottom left - :param width: width of widget. Defaults to texture width if not specified. - :param height: height of widget. Defaults to texture height if not specified. - :param texture: texture to display for the widget. - :param texture_hovered: different texture to display if mouse is hovering over button. - :param texture_pressed: different texture to display if mouse button is pressed - while hovering over button. - :param text: text to add to the button. - :param multiline: allows to wrap text, if not enough width available - :param style: Used to style the button for different states. - :param scale: scale the button, based on the base texture size. - :param size_hint: Tuple of floats (0.0-1.0), how much space of the parent should be requested - :param size_hint_min: min width and height in pixel - :param size_hint_max: max width and height in pixel + """A button with an image for the face of the button. + + There are four states of the UITextureButton i.e.normal, hovered, pressed and disabled. + + Args: + x: x coordinate of bottom left + y: y coordinate of bottom left + width: width of widget. Defaults to texture width if not + specified. + height: height of widget. Defaults to texture height if not + specified. + texture: texture to display for the widget. + texture_hovered: different texture to display if mouse is + hovering over button. + texture_pressed: different texture to display if mouse button is + pressed while hovering over button. + text: text to add to the button. + multiline: allows to wrap text, if not enough width available + style: Used to style the button for different states. + scale: scale the button, based on the base texture size. + size_hint: Tuple of floats (0.0-1.0), how much space of the + parent should be requested + size_hint_min: min width and height in pixel + size_hint_max: max width and height in pixel """ _textures: dict[str, Union[Texture, NinePatchTexture]] = DictProperty() # type: ignore @@ -146,7 +149,7 @@ def __init__( bind(self, "_textures", self.trigger_render) def get_current_state(self) -> str: - """Returns the current state of the button i.e disabled, press, hover or normal.""" + """Returns the current state of the button i.e.disabled, press, hover or normal.""" if self.disabled: return "disabled" elif self.pressed: @@ -187,6 +190,7 @@ def texture_pressed(self, value: Texture): self.trigger_render() def do_render(self, surface: Surface): + """Render the widgets graphical representation.""" self.prepare_render(surface) style = self.get_current_style() @@ -200,9 +204,7 @@ def do_render(self, surface: Surface): surface.draw_texture(0, 0, self.content_width, self.content_height, current_texture) def _apply_style(self, style: UITextureButtonStyle): - """ - Callback which is called right before rendering to apply changes for rendering. - """ + """Callback which is called right before rendering to apply changes for rendering.""" font_name = style.get("font_name", UIFlatButton.UIStyle.font_name) font_size = style.get("font_size", UIFlatButton.UIStyle.font_size) font_color = style.get("font_color", UIFlatButton.UIStyle.font_color) @@ -212,25 +214,25 @@ def _apply_style(self, style: UITextureButtonStyle): class UIFlatButton(UIInteractiveWidget, UIStyledWidget, UITextWidget): - """ - A text button, with support for background color and a border. - - There are four states of the UITextureButton i.e normal, hovered, pressed and disabled. - - :param x: x coordinate of bottom left - :param y: y coordinate of bottom left - :param width: width of widget. Defaults to texture width if not specified. - :param height: height of widget. Defaults to texture height if not specified. - :param text: text to add to the button. - :param multiline: allows to wrap text, if not enough width available - :param style: Used to style the button - + """A text button, with support for background color and a border. + + There are four states of the UITextureButton i.e. normal, hovered, pressed and disabled. + + Args: + x: x coordinate of bottom left + y: y coordinate of bottom left + width: width of widget. Defaults to texture width if not + specified. + height: height of widget. Defaults to texture height if not + specified. + text: text to add to the button. + multiline: allows to wrap text, if not enough width available + style: Used to style the button """ @dataclass class UIStyle(UIStyleBase): - """ - Used to style the button. Below is its use case. + """Used to style the button. Below is its use case. .. code:: py @@ -302,7 +304,7 @@ def __init__( ) def get_current_state(self) -> str: - """Returns the current state of the button i.e disabled, press, hover or normal.""" + """Returns the current state of the button i.e.disabled, press, hover or normal.""" if self.disabled: return "disabled" elif self.pressed: @@ -313,6 +315,7 @@ def get_current_state(self) -> str: return "normal" 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() @@ -339,9 +342,7 @@ def do_render(self, surface: Surface): ) def _apply_style(self, style: UIStyle): - """ - Callback which is called right before rendering to apply changes for rendering. - """ + """Callback which is called right before rendering to apply changes for rendering.""" font_name = style.get("font_name", UIFlatButton.UIStyle.font_name) font_size = style.get("font_size", UIFlatButton.UIStyle.font_size) font_color = style.get("font_color", UIFlatButton.UIStyle.font_color) diff --git a/arcade/gui/widgets/dropdown.py b/arcade/gui/widgets/dropdown.py index 3719d7e37a..08e37c1521 100644 --- a/arcade/gui/widgets/dropdown.py +++ b/arcade/gui/widgets/dropdown.py @@ -15,8 +15,7 @@ class _UIDropdownOverlay(UIBoxLayout): - """ - Represents the dropdown options overlay. + """Represents the dropdown options overlay. Currently only handles closing the overlay when clicked outside of the options. """ @@ -40,8 +39,7 @@ def on_event(self, event: UIEvent) -> Optional[bool]: class UIDropdown(UILayout): - """ - A dropdown layout. When clicked displays a list of options provided. + """A dropdown layout. When clicked displays a list of options provided. Triggers an event when an option is clicked, the event can be read by @@ -53,13 +51,14 @@ class UIDropdown(UILayout): def on_change(event: UIOnChangeEvent): print(event.old_value, event.new_value) - :param x: x coordinate of bottom left - :param y: y coordinate of bottom left - :param width: Width of each of the option. - :param height: Height of each of the option. - :param default: The default value shown. - :param options: The options displayed when the layout is clicked. - :param style: Used to style the dropdown. + Args: + x: x coordinate of bottom left + y: y coordinate of bottom left + width: Width of each of the option. + 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 @@ -171,6 +170,8 @@ def _on_option_click(self, event: UIOnClickEvent): self._overlay.hide() def do_layout(self): + """Position the overlay, this is not a common thing to do in do_layout, + but is required for the dropdown.""" self._default_button.rect = self.rect # resize layout to contain widgets @@ -182,8 +183,7 @@ def do_layout(self): self._overlay.rect = rect.align_top(self.bottom - 2).align_left(self._default_button.left) def on_change(self, event: UIOnChangeEvent): - """ - To be implemented by the user, triggered when the current selected value + """To be implemented by the user, triggered when the current selected value is changed to a different option. """ pass diff --git a/arcade/gui/widgets/image.py b/arcade/gui/widgets/image.py index d336996c42..1df3e4151c 100644 --- a/arcade/gui/widgets/image.py +++ b/arcade/gui/widgets/image.py @@ -2,6 +2,8 @@ from typing import Union +from typing_extensions import override + from arcade import Texture from arcade.gui import NinePatchTexture from arcade.gui.property import Property, bind @@ -10,8 +12,15 @@ class UIImage(UIWidget): - """ - UIWidget showing a texture. + """UIWidget showing a texture. + + If no size given, the texture size is used. + + Args: + texture: Texture to show + width: width of widget + height: height of widget + **kwargs: passed to UIWidget """ texture: Union[Texture, NinePatchTexture] = Property() # type: ignore @@ -20,14 +29,22 @@ def __init__( self, *, texture: Union[Texture, NinePatchTexture], + width: float | None = None, + height: float | None = None, **kwargs, ): self.texture = texture - super().__init__(**kwargs) + super().__init__( + width=width if width else texture.width, + height=height if height else texture.height, + **kwargs, + ) bind(self, "texture", self.trigger_render) + @override def do_render(self, surface: Surface): + """Render the stored texture in the size of the widget.""" self.prepare_render(surface) if self.texture: surface.draw_texture( diff --git a/arcade/gui/widgets/layout.py b/arcade/gui/widgets/layout.py index 41d01c4ecb..b3b93997a7 100644 --- a/arcade/gui/widgets/layout.py +++ b/arcade/gui/widgets/layout.py @@ -2,6 +2,8 @@ from typing import Iterable, Optional, TypeVar, cast +from typing_extensions import override + from arcade.gui.property import bind, unbind from arcade.gui.widgets import UILayout, UIWidget @@ -11,8 +13,7 @@ class UIAnchorLayout(UILayout): - """ - Places children based on anchor values. + """Places children based on anchor values. Defaults to ``size_hint = (1, 1)``. @@ -44,6 +45,17 @@ class UIAnchorLayout(UILayout): - ``align_y``: ``float`` = 0 Vertical alignement for the layout. + + Args: + x: ``x`` coordinate of the bottom left corner. + y: ``y`` coordinate of the bottom left corner. + width: Width of the layout. + height: Height of the layout. + children: Initial list of children. More can be added later. + size_hint: Size hint for :py:class:`~arcade.gui.UILayout` + size_hint_min: Minimum width and height in pixels. + size_hint_max: Maximum width and height in pixels. + **kwargs: Additional keyword arguments passed to UILayout. """ default_anchor_x = "center" @@ -75,6 +87,9 @@ def __init__( ) def do_layout(self): + """Executes the layout algorithm. + + Children are placed based on their anchor values.""" for child, data in self._children: self._place_child(child, **data) @@ -88,23 +103,24 @@ def add( align_y: float = 0, **kwargs, ) -> W: - """ - Add a widget to the layout as a child. Added widgets will receive + """Add a widget to the layout as a child. Added widgets will receive all user-interface events and be rendered. By default, the latest added widget will receive events first and will be rendered on top of others. The widgets will be automatically placed within this widget. - :param child: Specified child widget to add. - :param anchor_x: Horizontal anchor. Valid options are ``left``, - ``right``, and ``center``. - :param align_x: Offset or padding for the horizontal anchor. - :param anchor_y: Vertical anchor. Valid options are ``top``, - ``center``, and ``bottom``. - :param align_y: Offset or padding for the vertical anchor. - - :return: Given child that was just added to the layout. + Args: + child: Specified child widget to add. + anchor_x: Horizontal anchor. Valid options are ``left``, + ``right``, and ``center``. + align_x: Offset or padding for the horizontal anchor. + anchor_y: Vertical anchor. Valid options are ``top``, + ``center``, and ``bottom``. + align_y: Offset or padding for the vertical anchor. + + Returns: + Given child that was just added to the layout. """ return super().add( child=child, @@ -171,8 +187,7 @@ def _place_child( class UIBoxLayout(UILayout): - """ - Place widgets next to each other. Depending on the + """Place widgets next to each other. Depending on the :py:class:`~arcade.gui.UIBoxLayout.vertical` attribute, the widgets are placed top to bottom or left to right. @@ -196,18 +211,19 @@ class UIBoxLayout(UILayout): children) will be distributed to the child widgets based on their ``size_hint``. - :param x: ``x`` coordinate of the bottom left corner. - :param y: ``y`` coordinate of the bottom left corner. - :param vertical: Layout children vertical (True) or horizontal (False). - :param align: Align children in orthogonal direction:: - - ``x``: ``left``, ``center``, and ``right`` - - ``y``: ``top``, ``center``, and ``bottom`` - :param children: Initial list of children. More can be added later. - :param size_hint: Size hint for the :py:class:`~arcade.gui.UILayout` if - the widget would like to grow. Defaults to ``0, 0`` -> - minimal size to contain children. - :param size_hint_max: Maximum width and height in pixels. - :param space_between: Space in pixels between the children. + Args: + x: ``x`` coordinate of the bottom left corner. + y: ``y`` coordinate of the bottom left corner. + vertical: Layout children vertical (True) or horizontal (False). + align: Align children in orthogonal direction:: - ``x``: + ``left``, ``center``, and ``right`` - ``y``: ``top``, + ``center``, and ``bottom`` + children: Initial list of children. More can be added later. + size_hint: Size hint for the :py:class:`~arcade.gui.UILayout` if + the widget would like to grow. Defaults to ``0, 0`` -> + minimal size to contain children. + size_hint_max: Maximum width and height in pixels. + space_between: Space in pixels between the children. """ def __init__( @@ -254,7 +270,13 @@ def __init__( self._update_size_hints() + @override def add(self, child: W, **kwargs) -> W: + """Add a widget to this layout + + Args: + child: The widget to add to the layout. + """ # subscribe to child's changes, which might affect the own size hint bind(child, "_children", self._trigger_size_hint_update) bind(child, "rect", self._trigger_size_hint_update) @@ -264,7 +286,9 @@ def add(self, child: W, **kwargs) -> W: return super().add(child, **kwargs) + @override def remove(self, child: "UIWidget"): + """Remove a child from the layout.""" # unsubscribe from child's changes unbind(child, "_children", self._trigger_size_hint_update) unbind(child, "rect", self._trigger_size_hint_update) @@ -300,9 +324,8 @@ def _update_size_hints(self): self.size_hint_min = base_width + width, base_height + height def fit_content(self): - """ - Resize the layout to fit the content. This will take the minimal required size into account. - """ + """Resize the layout to fit the content. + This will take the minimal required size into account.""" self._update_size_hints() self.rect = self.rect.resize(self.size_hint_min[0], self.size_hint_min[1]) @@ -314,6 +337,13 @@ def prepare_layout(self): self._update_size_hints() def do_layout(self): + """Executes the layout algorithm. + + This method is called by the parent layout to place the children, after the rect was set. + + The layout algorithm will place the children based on the size hints next to each other. + Depending on the vertical attribute, the children are placed top to bottom or left to right. + """ start_y = self.content_rect.top start_x = self.content_rect.left @@ -450,8 +480,7 @@ def do_layout(self): class UIGridLayout(UILayout): - """ - Place widgets in a grid layout. This is similar to tkinter's ``grid`` + """Place widgets in a grid layout. This is similar to tkinter's ``grid`` layout geometry manager. Defaults to ``size_hint = (0, 0)``. @@ -463,24 +492,23 @@ class UIGridLayout(UILayout): ``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. - :param x: ``x`` coordinate of bottom left corner. - :param y: ``y`` coordinate of bottom left corner. - :param align_horizontal: Align children in orthogonal direction. - Options include ``left``, ``center``, and - ``right``. - :param align_vertical: Align children in orthogonal direction. Options - include ``top``, ``center``, and ``bottom``. - :param children: Initial list of children. More can be - added later. - :param size_hint: A size hint for :py:class:`~arcade.gui.UILayout`, if the - :py:class:`~arcade.gui.UIWidget` would like to grow. - :param size_hint_max: Maximum width and height in pixels. - :param horizontal_spacing: Space between columns. - :param vertical_spacing: Space between rows. - :param column_count: Number of columns in the grid. This can be changed - later. - :param row_count: Number of rows in the grid. This can be changed - later. + Args: + x: ``x`` coordinate of bottom left corner. + y: ``y`` coordinate of bottom left corner. + align_horizontal: Align children in orthogonal direction. + Options include ``left``, ``center``, and ``right``. + align_vertical: Align children in orthogonal direction. Options + include ``top``, ``center``, and ``bottom``. + children: Initial list of children. More can be added later. + size_hint: A size hint for :py:class:`~arcade.gui.UILayout`, if + the :py:class:`~arcade.gui.UIWidget` would like to grow. + size_hint_max: Maximum width and height in pixels. + horizontal_spacing: Space between columns. + vertical_spacing: Space between rows. + column_count: Number of columns in the grid. This can be changed + later. + row_count: Number of rows in the grid. This can be changed + later. """ def __init__( @@ -541,16 +569,16 @@ def add( row_span: int = 1, **kwargs, ) -> W: - """ - Add a widget to the grid layout. - - :param child: Specified widget to add as a child of the layout. - :param col_num: Column index in which the widget is to be added. - The first column at the left of the widget starts at 0. - :param row_num: The row number in which the widget is to be added. - The first row at the top of the layout is numbered 0. - :param col_span: Number of columns the widget will stretch for. - :param row_span: Number of rows the widget will stretch for. + """Add a widget to the grid layout. + + Args: + child: Specified widget to add as a child of the layout. + col_num: 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. + The first row at the top of the layout is numbered 0. + col_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 bind(child, "_children", self._trigger_size_hint_update) @@ -569,6 +597,7 @@ def add( ) def remove(self, child: "UIWidget"): + """Remove a child from the layout.""" # unsubscribe from child's changes unbind(child, "_children", self._trigger_size_hint_update) unbind(child, "rect", self._trigger_size_hint_update) @@ -636,6 +665,9 @@ def _update_size_hints(self): 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 @@ -702,9 +734,11 @@ def do_layout(self): ) def ratio(dimensions: list) -> list: - """ - Used to calculate ratio of the elements based on the minimum value in the parameter. - :param dimension: List containing max height or width of the cells. + """Used to calculate ratio of the elements based on the minimum value in the parameter. + + 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] diff --git a/arcade/gui/widgets/slider.py b/arcade/gui/widgets/slider.py index d20a60d28f..5569d624eb 100644 --- a/arcade/gui/widgets/slider.py +++ b/arcade/gui/widgets/slider.py @@ -5,6 +5,7 @@ from typing import Mapping, Optional, Union from pyglet.event import EVENT_HANDLED, EVENT_UNHANDLED +from typing_extensions import override import arcade from arcade import Texture @@ -23,8 +24,7 @@ class UIBaseSlider(UIInteractiveWidget, metaclass=ABCMeta): - """ - Base class for sliders. + """Base class for sliders. A slider contains of a horizontal track and a thumb. The thumb can be moved along the track to set the value of the slider. @@ -32,6 +32,22 @@ class UIBaseSlider(UIInteractiveWidget, metaclass=ABCMeta): Use the `on_change` event to get notified about value changes. Subclasses should implement the `_render_track` and `_render_thumb` methods. + + Args: + + value: Current value of the curosr of the slider. + min_value: Minimum value of the slider. + max_value: Maximum value of the slider. + x: x coordinate of bottom left. + y: y coordinate of bottom left. + width: Width of the slider. + height: Height of the slider. + size_hint: Size hint of the slider. + size_hint_min: Minimum size hint of the slider. + size_hint_max: Maximum size hint of the slider. + style: Used to style the slider for different states. + **kwargs: Passed to UIInteractiveWidget. + """ value = Property(0) @@ -113,33 +129,49 @@ def _thumb_x(self, nx): self.content_width - 2 * self._cursor_width ) + @override def do_render(self, surface: Surface): + """Render the slider, including track and thumb.""" self.prepare_render(surface) self._render_track(surface) self._render_thumb(surface) @abstractmethod - def _render_track(self, surface): + def _render_track(self, surface: Surface): """Render the track of the slider. This method should be implemented in a slider implementation. Track should stay within self.content_rect. + + Args: + surface: Surface to render on. """ pass @abstractmethod - def _render_thumb(self, surface): + def _render_thumb(self, surface: Surface): """Render the thumb of the slider. This method should be implemented in a slider implementation. Thumb should stay within self.content_rect. x coordinate of the thumb should be self._thumb_x. + + Args: + surface: Surface to render on. """ pass + @override def on_event(self, event: UIEvent) -> Optional[bool]: + """ + Args: + event: Event to handle. + + Returns: True if event was handled, False otherwise. + + """ if super().on_event(event): return EVENT_HANDLED @@ -151,24 +183,44 @@ def on_event(self, event: UIEvent) -> Optional[bool]: return EVENT_UNHANDLED + @override def on_click(self, event: UIOnClickEvent): + """Handle click events to set the value of the slider. + + A new value is calculated based on the click position and the slider's width and + the `on_change` event is dispatched. + + Args: + event: Click event. + """ old_value = self.value self._thumb_x = event.x self.dispatch_event("on_change", UIOnChangeEvent(self, old_value, self.value)) # type: ignore def on_change(self, event: UIOnChangeEvent): - """To be implemented by the user, triggered when the cursor's value is changed.""" + """To be implemented by the user, triggered when the cursor's value is changed. + + Args: + event: Event containing the old and new value of the cursor. + """ pass @dataclass class UISliderStyle(UIStyleBase): - """ - Used to style the slider for different states. Below is its use case. + """Used to style the slider for different states. Below is its use case. .. code:: py button = UITextureButton(style={"normal": UITextureButton.UIStyle(...),}) + + Args: + bg: Background color. + border: Border color. + border_width: Width of the border. + filled_track: Color of the filled track. + unfilled_track: Color of the unfilled track. + """ bg: RGBA255 = Color(94, 104, 117) @@ -179,25 +231,24 @@ class UISliderStyle(UIStyleBase): class UISlider(UIStyledWidget[UISliderStyle], UIBaseSlider): - """ - A simple slider. + """A simple slider. A slider contains of a horizontal track and a thumb. The thumb can be moved along the track to set the value of the slider. Use the `on_change` event to get notified about value changes. - There are four states of the UISlider i.e normal, hovered, pressed and disabled. + There are four states of the UISlider i.e. normal, hovered, pressed and disabled. - :param value: Current value of the curosr of the slider. - :param min_value: Minimum value of the slider. - :param max_value: Maximum value of the slider. - :param x: x coordinate of bottom left. - :param y: y coordinate of bottom left. - :param width: Width of the slider. - :param height: Height of the slider. - :param Mapping[str, "UISlider.UIStyle"] | None style: Used to style the slider - for different states. + Args: + value: Current value of the cursor of the slider. + min_value: Minimum value of the slider. + max_value: Maximum value of the slider. + x: x coordinate of bottom left. + y: y coordinate of bottom left. + width: Width of the slider. + height: Height of the slider. + style: Used to style the slider for different states. """ @@ -259,8 +310,13 @@ def __init__( **kwargs, ) + @override def get_current_state(self) -> str: - """Returns the current state of the slider i.e disabled, press, hover or normal.""" + """Get the current state of the slider. + + Returns: + ""normal"", ""hover"", ""press"" or ""disabled"". + """ if self.disabled: return "disabled" elif self.pressed: @@ -270,8 +326,8 @@ def get_current_state(self) -> str: else: return "normal" + @override def _render_track(self, surface: Surface): - """Render the track of the slider.""" style = self.get_current_style() bg_slider_color = style.get("unfilled_track", UISlider.UIStyle.unfilled_track) @@ -300,8 +356,8 @@ def _render_track(self, surface: Surface): fg_slider_color, ) + @override def _render_thumb(self, surface: Surface): - """Render the thumb of the slider.""" style = self.get_current_style() border_width = style.get("border_width", UISlider.UIStyle.border_width) @@ -327,8 +383,7 @@ def _render_thumb(self, surface: Surface): class UITextureSlider(UISlider): - """ - A custom slider subclass which supports textures. + """A custom slider subclass which supports textures. You can copy this as-is into your own project, or you can modify the class to have more features as needed. @@ -346,6 +401,7 @@ def __init__( 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) @@ -366,6 +422,7 @@ def _render_track(self, surface: Surface): style.filled_track, ) + @override def _render_thumb(self, surface: Surface): cursor_center_x = self._thumb_x rel_cursor_x = cursor_center_x - self.left diff --git a/arcade/gui/widgets/text.py b/arcade/gui/widgets/text.py index 073b352a4f..9a7c04d6c4 100644 --- a/arcade/gui/widgets/text.py +++ b/arcade/gui/widgets/text.py @@ -6,6 +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 import arcade from arcade.gui.events import ( @@ -39,35 +40,37 @@ class UILabel(UIWidget): If the text changes frequently, ensure to set a background color or texture, which will prevent a full rendering of the whole UI and only render the label itself. - :param text: Text displayed on the label. - :param x: x position (default anchor is bottom-left). - :param y: y position (default anchor is bottom-left). - :param width: Width of the label. Defaults to text width if not - specified. See - :py:meth:`~pyglet.text.layout.TextLayout.content_width`. - :param height: Height of the label. Defaults to text height if not - specified. See - :py:meth:`~pyglet.text.layout.TextLayout.content_height`. - :param font_name: A list of fonts to use. Arcade will start at the beginning - of the tuple and keep trying to load fonts until success. - :param font_size: Font size of font. - :param text_color: Color of the text. - :param bold: If enabled, the label's text will be in a **bold** style. - :param italic: If enabled, the label's text will be in an *italic* - style. - :param stretch: Stretch font style. - :param align: Horizontal alignment of text on a line. This only applies - if a width is supplied. Valid options include ``"left"``, - ``"center"`` or ``"right"``. - :param dpi: Resolution of the fonts in the layout. Defaults to 96. - :param multiline: If enabled, a ``\\n`` will start a new line. Changing text or - font will require a manual call of :py:meth:`~arcade.gui.UILabel.fit_content` - to prevent text line wrap. - :param size_hint: A tuple of floats between 0 and 1 defining the amount of - space of the parent should be requested. Default (0, 0) which fits the content. - :param size_hint_max: Maximum size hint width and height in pixel. - :param style: Not used. Labels will have no need for a style; they are too - simple (just a text display). + Args: + text: Text displayed on the label. + x: x position (default anchor is bottom-left). + y: y position (default anchor is bottom-left). + width: Width of the label. Defaults to text width if not + specified. See + :py:meth:`~pyglet.text.layout.TextLayout.content_width`. + height: Height of the label. Defaults to text height if not + specified. See + :py:meth:`~pyglet.text.layout.TextLayout.content_height`. + font_name: A list of fonts to use. Arcade will start at the + beginning of the tuple and keep trying to load fonts until + success. + font_size: Font size of font. + text_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* + style. + stretch: Stretch font style. + align: Horizontal alignment of text on a line. This only applies + if a width is supplied. Valid options include ``"left"``, + ``"center"`` or ``"right"``. + dpi: Resolution of the fonts in the layout. Defaults to 96. + multiline: If enabled, a ``\\n`` will start a new line. Changing + text or font will require a manual call of + :py:meth:`~arcade.gui.UILabel.fit_content` to prevent text + line wrap. + size_hint: A tuple of floats between 0 and 1 defining the amount + of space of the parent should be requested. Default (0, 0) + which fits the content. + size_hint_max: Maximum size hint width and height in pixel. """ ADAPTIVE_MULTILINE_WIDTH = 999999 @@ -82,7 +85,7 @@ def __init__( height: Optional[float] = None, font_name=("Arial",), font_size: float = 12, - text_color: RGBOrA255 = (255, 255, 255, 255), + text_color: RGBOrA255 = arcade.color.WHITE, bold=False, italic=False, align="left", @@ -149,8 +152,7 @@ def __init__( self._update_size_hint_min() def fit_content(self): - """ - Manually set the width and height of the label to contain the whole text. + """Manually set the width and height of the label to contain the whole text. Based on the size_hint_min. If multiline is enabled, the width will be calculated based on longest line of the text. @@ -170,12 +172,12 @@ def fit_content(self): @property def text(self): + """Text of the label.""" return self._label.text @text.setter def text(self, value): - """ - Update text of the label. + """Update text of the label. This triggers a full render to ensure that previous text is cleared out. """ @@ -190,8 +192,7 @@ def text(self, value): self.trigger_full_render() def _update_label(self): - """ - Update the position and size of the label. + """Update the position and size of the label. So it fits into the content area of the widget. Should always be called after the content area changed. @@ -206,6 +207,7 @@ def _update_label(self): label.height = int(self.content_height) def _update_size_hint_min(self): + """Update the minimum size hint based on the label content size.""" min_width = self._label.content_width + 1 # +1 required to prevent line wrap min_width += self._padding_left + self._padding_right + 2 * self._border_width @@ -220,8 +222,14 @@ def update_font( font_size: Optional[float] = None, font_color: Optional[Color] = None, ): - """ - Update font of the label. + """Update font of the label. + + Args: + font_name: A list of fonts to use. Arcade will start at the + beginning of the tuple and keep trying to load fonts until + success. + font_size: Font size of font. + font_color: Color of the text. """ font_name = font_name or self._label.font_name font_size = font_size or self._label.font_size @@ -250,6 +258,7 @@ def multiline(self) -> bool: return self._label.multiline def do_render(self, surface: Surface): + """Render the label via py:class:`~arcade.Text`.""" self.prepare_render(surface) # pyglet rendering automatically applied by arcade.Text @@ -257,13 +266,24 @@ def do_render(self, surface: Surface): class UITextWidget(UIAnchorLayout): - """ - Adds the ability to add text to a widget. + """Adds the ability to add text to a widget. Use this to create subclass widgets, which have text. The text can be placed within the widget using :py:class:`~arcade.gui.UIAnchorLayout` parameters with :py:meth:`~arcade.gui.UITextWidget.place_text`. + + The widget holds reference to one primary :py:class:`~arcade.gui.UILabel`, which is placed in + the widget's layout. This label can be accessed + via :py:attr:`~arcade.gui.UITextWidget.ui_label`. + + To change font, font size, or text color, use py:meth:`~arcade.gui.UILabel.update_font`. + + Args: + text: Text displayed on the label. + multiline: If enabled, a ``\\n`` will start a new line. + **kwargs: passed to :py:class:`~arcade.gui.UIWidget`. + """ def __init__(self, *, text: str, multiline: bool = False, **kwargs): @@ -280,13 +300,21 @@ def place_text( anchor_y: Optional[str] = None, align_y: float = 0, **kwargs, - ): - """ - Place widget's text within the widget using + ) -> UILabel: + """Place widget's text within the widget using :py:class:`~arcade.gui.UIAnchorLayout` parameters. + + Args: + anchor_x: Horizontal anchor. Valid options are ``left``, + ``right``, and ``center``. + align_x: Offset or padding for the horizontal anchor. + anchor_y: Vertical anchor. Valid options are ``top``, + ``center``, and ``bottom``. + align_y: Offset or padding for the vertical anchor. + **kwargs: Additional keyword arguments passed to the layout function. """ self.remove(self._label) - self.add( + return self.add( child=self._label, anchor_x=anchor_x, align_x=align_x, @@ -297,8 +325,7 @@ def place_text( @property def text(self): - """ - Text of the widget. Modifying this repeatedly will cause significant + """Text of the widget. Modifying this repeatedly will cause significant lag; calculating glyph position is very expensive. """ return self.ui_label.text @@ -310,8 +337,7 @@ def text(self, value): @property def multiline(self): - """ - Get or set the multiline mode. + """Get or set the multiline mode. Newline characters (``"\\n"``) will only be honored when this is set to ``True``. If you want a scrollable text widget, please use :py:class:`~arcade.gui.UITextArea` @@ -321,15 +347,14 @@ def multiline(self): @property def ui_label(self) -> UILabel: - """ - Internal py:class:`~arcade.gui.UILabel` used for rendering the text. - """ + """Internal py:class:`~arcade.gui.UILabel` used for rendering the text.""" return self._label class UIInputText(UIWidget): - """ - An input field the user can type text into. This is useful in returning + """An input field the user can type text into. + + This is useful in returning string input from the user. A caret is displayed, which the user can move around with a mouse or keyboard. @@ -337,28 +362,30 @@ class UIInputText(UIWidget): around the caret. Arcade confirms that the field is active before allowing users to type, so it is okay to have multiple of these. - :param x: x position (default anchor is bottom-left). - :param y: y position (default anchor is bottom-left). - :param width: Width of the text field. - :param height: Height of the text field. - :param text: Initial text displayed. This can be modified later - programmatically or by the user's interaction with the caret. - :param font_name: A list of fonts to use. Arcade will start at the beginning - of the tuple and keep trying to load fonts until success. - :param font_size: Font size of font. - :param text_color: Color of the text. - :param multiline: If enabled, a ``\\n`` will start a new line. A - :py:class:`~arcade.gui.UITextWidget` ``multiline`` of - True is the same thing as - a :py:class:`~arcade.gui.UITextArea`. - :param caret_color: An RGBA or RGB color for the caret with each - channel between 0 and 255, inclusive. - :param size_hint: A tuple of floats between 0 and 1 defining the amount of - space of the parent should be requested. - :param size_hint_min: Minimum size hint width and height in pixel. - :param size_hint_max: Maximum size hint width and height in pixel. - :param style: Style has not been implemented for this widget, however it - will be added in the near future. + Args: + x: x position (default anchor is bottom-left). + y: y position (default anchor is bottom-left). + width: Width of the text field. + height: Height of the text field. + text: Initial text displayed. This can be modified later + programmatically or by the user's interaction with the + caret. + font_name: A list of fonts to use. Arcade will start at the + beginning of the tuple and keep trying to load fonts until + success. + font_size: Font size of font. + text_color: Color of the text. + multiline: If enabled, a ``\\n`` will start a new line. A + :py:class:`~arcade.gui.UITextWidget` ``multiline`` of True + is the same thing as a :py:class:`~arcade.gui.UITextArea`. + caret_color: An RGBA or RGB color for the caret with each + channel between 0 and 255, inclusive. + size_hint: A tuple of floats between 0 and 1 defining the amount + 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. + **kwargs: passed to :py:class:`~arcade.gui.UIWidget`. + """ # Move layout one pixel into the scissor box so the caret is also shown at @@ -375,9 +402,9 @@ def __init__( text: str = "", font_name=("Arial",), font_size: float = 12, - text_color: RGBOrA255 = (0, 0, 0, 255), + text_color: RGBOrA255 = arcade.color.WHITE, multiline=False, - caret_color: RGBOrA255 = (0, 0, 0, 255), + caret_color: RGBOrA255 = arcade.color.WHITE, size_hint=None, size_hint_min=None, size_hint_max=None, @@ -395,7 +422,7 @@ def __init__( ) self._active = False - self._text_color = text_color if len(text_color) == 4 else (*text_color, 255) + self._text_color = Color.from_iterable(text_color) self.doc: AbstractDocument = pyglet.text.decode_text(text) self.doc.set_style( @@ -422,19 +449,25 @@ 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 + @override def on_update(self, dt): + """Update the caret blink state.""" # Only trigger render if blinking state changed current_state = self._get_caret_blink_state() if self._blink_state != current_state: self._blink_state = current_state self.trigger_full_render() + @override def on_event(self, event: UIEvent) -> Optional[bool]: + """Handle events for the text input field. + + Text input is only active when the user clicks on the input field.""" # If not active, check to activate, return if not self._active and isinstance(event, UIMousePressEvent): if self.rect.point_in_rect(event.pos): self.activate() - return EVENT_UNHANDLED + return EVENT_HANDLED # If active check to deactivate if self._active and isinstance(event, UIMousePressEvent): @@ -478,6 +511,9 @@ def on_event(self, event: UIEvent) -> Optional[bool]: @property def active(self) -> bool: + """Return if the text input field is active. + + An active text input field will show a caret and accept text input.""" return self._active def activate(self): @@ -510,6 +546,7 @@ def _update_layout(self): @property def text(self): + """Text of the input field.""" return self.doc.text @text.setter @@ -517,7 +554,9 @@ def text(self, value): self.doc.text = value self.trigger_full_render() + @override def do_render(self, surface: Surface): + """Render the text input field.""" self._update_layout() self.prepare_render(surface) @@ -525,27 +564,26 @@ def do_render(self, surface: Surface): class UITextArea(UIWidget): - """ - A text area that allows users to view large documents of text by scrolling + """A text area that allows users to view large documents of text by scrolling the mouse. - :param x: x position (default anchor is bottom-left). - :param y: y position (default anchor is bottom-left). - :param width: Width of the text area. - :param height: Height of the text area. - :param text: Initial text displayed. - :param font_name: A list of fonts to use. Arcade will start at the beginning - of the tuple and keep trying to load fonts until success. - :param font_size: Font size of font. - :param text_color: Color of the text. - :param multiline: If enabled, a ``\\n`` will start a new line. - :param scroll_speed: Speed of mouse scrolling. - :param size_hint: A tuple of floats between 0 and 1 defining the amount of - space of the parent should be requested. - :param size_hint_min: Minimum size hint width and height in pixel. - :param size_hint_max: Maximum size hint width and height in pixel. - :param style: Style has not been implemented for this widget, however it - will be added in the near future. + Args: + x: x position (default anchor is bottom-left). + y: y position (default anchor is bottom-left). + width: Width of the text area. + height: Height of the text area. + text: Initial text displayed. + font_name: A list of fonts to use. Arcade will start at the + beginning of the tuple and keep trying to load fonts until + success. + font_size: Font size of font. + text_color: Color of the text. + multiline: If enabled, a ``\\n`` will start a new line. + scroll_speed: Speed of mouse scrolling. + size_hint: A tuple of floats between 0 and 1 defining the amount + 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. """ def __init__( @@ -558,7 +596,7 @@ def __init__( text: str = "", font_name=("Arial",), font_size: float = 12, - text_color: RGBA255 = (255, 255, 255, 255), + text_color: RGBA255 = arcade.color.WHITE, multiline: bool = True, scroll_speed: Optional[float] = None, size_hint=None, @@ -602,9 +640,7 @@ def __init__( # bind(self, "rect", self._update_layout) def fit_content(self): - """ - Set the width and height of the text area to contain the whole text. - """ + """Set the width and height of the text area to contain the whole text.""" self.rect = LBWH( self.left, self.bottom, @@ -614,6 +650,7 @@ def fit_content(self): @property def text(self): + """Text of the text area.""" return self.doc.text @text.setter @@ -634,12 +671,16 @@ def _update_layout(self): layout.height = content_height layout.end_update() + @override def do_render(self, surface: Surface): + """Render the text area.""" self._update_layout() self.prepare_render(surface) self.layout.draw() + @override def on_event(self, event: UIEvent) -> Optional[bool]: + """Handle scrolling of the widget.""" if isinstance(event, UIMouseScrollEvent): if self.rect.point_in_rect(event.pos): self.layout.view_y += event.scroll_y * self.scroll_speed # type: ignore # pending https://github.com/pyglet/pyglet/issues/916 diff --git a/arcade/gui/widgets/toggle.py b/arcade/gui/widgets/toggle.py index 150658a36c..1d407a452a 100644 --- a/arcade/gui/widgets/toggle.py +++ b/arcade/gui/widgets/toggle.py @@ -3,6 +3,7 @@ from typing import Optional from PIL import ImageEnhance +from typing_extensions import override from arcade import Texture from arcade.gui.events import UIOnChangeEvent, UIOnClickEvent @@ -12,12 +13,23 @@ class UITextureToggle(UIInteractiveWidget): - """ - A toggle button switching between on (True) and off (False) state. + """A toggle button switching between on (True) and off (False) state. on_texture and off_texture are required. State dependent textures are generated by changing the brightness (hover, press) of the provided textures or converting them to grayscale (disabled). + + Args: + x: x coordinate of bottom left + y: y coordinate of bottom left + width: Width of the button. + height: Height of the button. + on_texture: Texture to show when the button is on. + off_texture: Texture to show when the button is off. + value: Initial value of the button. + size_hint: Size hint for the layout. + size_hint_min: Minimum size hint for the layout. + size_hint_max: Maximum size hint for the layout. """ # Experimental ui class @@ -91,10 +103,14 @@ def __init__( def _dispatch_on_change_event(self): self.dispatch_event("on_change", UIOnChangeEvent(self, not self.value, self.value)) + @override def on_click(self, event: UIOnClickEvent): + """Change the value of the button on click.""" self.value = not self.value + @override def do_render(self, surface: Surface): + """Render the button, using texture depending on the state.""" self.prepare_render(surface) tex = self.normal_on_tex if self.value else self.normal_off_tex if self.disabled: @@ -106,4 +122,9 @@ def do_render(self, surface: Surface): surface.draw_texture(0, 0, self.content_width, self.content_height, tex) def on_change(self, event: UIOnChangeEvent): + """To be implemented by the user, triggered when the cursor's value is changed. + + Args: + event: Event containing the old and new value of the cursor. + """ pass diff --git a/doc/programming_guide/gui/style.rst b/doc/programming_guide/gui/style.rst index a4bdfed7f0..f65675dfb4 100644 --- a/doc/programming_guide/gui/style.rst +++ b/doc/programming_guide/gui/style.rst @@ -179,7 +179,7 @@ Your own stylable widget } def get_current_state(self) -> str: - """Returns the current state of the widget i.e disabled, press, hover or normal.""" + """Returns the current state of the widget i.e.disabled, press, hover or normal.""" if self.disabled: return "disabled" elif self.pressed: diff --git a/doc/tutorials/menu/index.rst b/doc/tutorials/menu/index.rst index 55117dd263..27d0aaeb1e 100644 --- a/doc/tutorials/menu/index.rst +++ b/doc/tutorials/menu/index.rst @@ -201,7 +201,7 @@ later why are those parameters needed. We also need to change accordingly the places where we have used this class i.e options and volume ``on_click`` event listener. The layer parameter being set -1, means that this layer is always drawn on top i.e its the first layer. +1, means that this layer is always drawn on top i.e.its the first layer. .. literalinclude:: menu_05.py :caption: Editing arguments diff --git a/tests/unit/sprite/test_sprite_render.py b/tests/unit/sprite/test_sprite_render.py index 68ed1447f8..5fa02e2be7 100644 --- a/tests/unit/sprite/test_sprite_render.py +++ b/tests/unit/sprite/test_sprite_render.py @@ -1,9 +1,12 @@ """ Rendering tests for Sprite """ + import arcade -SPRITE_TEXTURE_FEMALE_PERSON_IDLE = arcade.load_texture(":resources:images/animated_characters/female_person/femalePerson_idle.png") +SPRITE_TEXTURE_FEMALE_PERSON_IDLE = arcade.load_texture( + ":resources:images/animated_characters/female_person/femalePerson_idle.png" +) SPRITE_TEXTURE_GOLD_COIN = arcade.load_texture(":resources:images/items/coinGold.png") @@ -31,7 +34,7 @@ def on_draw(): sprite_list.draw() assert arcade.get_pixel(50, 50) == (255, 204, 0) - window.on_draw = on_draw + window.draw = on_draw window.test(2) @@ -258,17 +261,26 @@ def test_render_sprite_remove(window): character_list = arcade.SpriteList() - sprite_1 = arcade.Sprite(":resources:images/animated_characters/female_person/femalePerson_idle.png", scale=CHARACTER_SCALING) + sprite_1 = arcade.Sprite( + ":resources:images/animated_characters/female_person/femalePerson_idle.png", + scale=CHARACTER_SCALING, + ) sprite_1.center_x = 150 sprite_1.center_y = 150 character_list.append(sprite_1) - sprite_2 = arcade.Sprite(":resources:images/animated_characters/female_person/femalePerson_idle.png", scale=CHARACTER_SCALING) + sprite_2 = arcade.Sprite( + ":resources:images/animated_characters/female_person/femalePerson_idle.png", + scale=CHARACTER_SCALING, + ) sprite_2.center_x = 250 sprite_2.center_y = 250 character_list.append(sprite_2) - sprite_3 = arcade.Sprite(":resources:images/animated_characters/female_person/femalePerson_idle.png", scale=CHARACTER_SCALING) + sprite_3 = arcade.Sprite( + ":resources:images/animated_characters/female_person/femalePerson_idle.png", + scale=CHARACTER_SCALING, + ) sprite_3.center_x = 250 sprite_3.center_y = 250 character_list.append(sprite_3) diff --git a/tests/unit/test_sound.py b/tests/unit/test_sound.py index 7d4783f2cd..50c6d4bdfd 100644 --- a/tests/unit/test_sound.py +++ b/tests/unit/test_sound.py @@ -101,7 +101,7 @@ def test_sound_play_sound_type_errors(window): arcade.play_sound(object()) assert ctx.value.args[0].endswith("arcade.Sound.") - #Pathlike raises and provides full loading guidance. + # Pathlike raises and provides full loading guidance. with pytest.raises(TypeError) as ctx: arcade.play_sound("file.wav") assert ctx.value.args[0].endswidth("play_sound.") diff --git a/util/convert_rst_to_google_docs.py b/util/convert_rst_to_google_docs.py deleted file mode 100644 index 038d07c101..0000000000 --- a/util/convert_rst_to_google_docs.py +++ /dev/null @@ -1,72 +0,0 @@ -""" -Attempt to convert the docstrings in the Arcade codebase from RST to Google Docs format. - -https://github.com/pythonarcade/arcade/issues/1797 -""" -print("Note: this is partially complete, use convert_rst_to_google_docs.sh instead!") -exit(1) - -import glob -import re - -fn_signature_w_docstring_regexp = re.compile( - r""" - (?P - (?P - def \s+ - (?P \S+ ) # Function name - \( .*? \) # Parameter list - ) - (?P # Return type annotation - \s* -> \s* - (?P .{1,300}? ) - )? - : - ) - [\r?\n\s]+ - (?P - \"\"\" # Docstring start - (?P .*? ) - \"\"\" # Docstring end - ) - """, - re.X | re.DOTALL) -docstring_rtype_regexp = re.compile( - r""" - \ + - :rtype: \s* (?P .*? ) \r?\n - """, - re.X | re.DOTALL) - -count = 0 -for file in [*glob.glob('arcade/*.py'), *glob.glob('arcade/**/*.py')]: - with open(file, "r") as f: - content = f.read() - pos = 0 - while True: - match = fn_signature_w_docstring_regexp.search(content, pos=pos) - if match: - offset = 0 - match2 = docstring_rtype_regexp.search(match.group('docstring')) - if match2: - # print(match.groupdict() | match2.groupdict()) - # print(match.group('fn_signature') + match.group('docstring')) - count += 1 - if match2: - # Remove rtype annotation from docstring - range = match.start('docstring') + match2.start(), match.start('docstring') + match2.end() - offset -= range[1] - range[0] - content = content[:range[0]] + content[range[1]:] - if match2 and not match.group('rtype_anno'): - print(file, match.group('fn_name')) - insert = f" -> {match2.group('rtype')}" - pos = match.end('fn_signature_before') - content = content[:pos] + insert + content[pos:] - offset += len(insert) - - pos = match.end() + offset - else: - break - with open(file, "w") as f: - f.write(content) -print(count) \ No newline at end of file diff --git a/util/convert_rst_to_google_docs.sh b/util/convert_rst_to_google_docs.sh deleted file mode 100755 index a622a6de24..0000000000 --- a/util/convert_rst_to_google_docs.sh +++ /dev/null @@ -1,78 +0,0 @@ -#!/bin/bash - -# Easy Wrapper around the docconvert python package -# https://pypi.org/project/docconvert/ - -requires() { -cat <&2 - requires 1>&2 - exit 1 -fi - - -Usage() { -cat < $CONFIG_FILE -{ - "input_style": "rest", - "output_style": "google", - "accepted_shebangs": [ - "python" - ], - "output": { - "first_line": true, - "remove_type_backticks": "false", - "use_types": false - } -} -EOF -} -ensure_have_config -echo "Attempting docconvert..." - -docconvert --config $CONFIG_FILE --in-place "$@"