Skip to content

Commit

Permalink
Gui/more docs (#2373)
Browse files Browse the repository at this point in the history
* Improve own_widget docs
* Add own_layout doc and list own_X examples
* Add UIWidget.visible flag to gui docs
* Clarify usage of Properties
  • Loading branch information
eruvanos authored Sep 28, 2024
1 parent 494f50c commit 4dbaacf
Show file tree
Hide file tree
Showing 9 changed files with 197 additions and 19 deletions.
116 changes: 116 additions & 0 deletions arcade/examples/gui/own_layout.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""Example of creating a custom progress bar.
This example shows how to create a custom layout.
The layout will place the widgets in a rotating circle around the center of the screen.
If arcade and Python are properly installed, you can run this example with:
python -m arcade.examples.gui.own_layout
"""

from __future__ import annotations

from math import cos, sin

import arcade
from arcade.gui import UIAnchorLayout, UIFlatButton, UILayout, UIView, UIWidget


class CircleLayout(UILayout):
"""A custom progress bar widget.
A UIAnchorLayout is a layout that arranges its children in a specific way.
The actual bar is a UISpace that fills the parent widget from left to right.
"""

def __init__(self, size_hint=(1, 1), **kwargs):
super().__init__(size_hint=size_hint, **kwargs)
self._time = 0 # used for rotation

def add(self, child: UIWidget, **kwargs) -> UIWidget:
"""Add a widget to the layout.
The widget is placed in a circle around the center of the screen.
"""
return super().add(child, **kwargs)

def do_layout(self):
"""Layout the children in a circle around the center of the screen."""
if not self._children:
return

# handle the size hints of the children
for child in self.children:
self._resize_child(child)

# calculate the radius, so that the children are placed inside the parent widget
max_child_width = max(child.content_width for child in self.children)
max_child_height = max(child.content_height for child in self.children)
radius = (
min(self.content_width - max_child_width, self.content_height - max_child_height) / 2
)

for i, child in enumerate(self.children):
angle = i / len(self.children) * 2 * 3.1415
# add rotation based on time
angle += self._time * 0.08
center_x = self.center_x + radius * cos(angle)
center_y = self.center_y + radius * sin(angle)

new_rect = child.rect.align_center((center_x, center_y))
child.rect = new_rect

def _resize_child(self, child: UIWidget):
"""Resizes the child based on the size_hint, size_hint_min, and size_hint_max."""
new_child_rect = child.rect

sh_w, sh_h = child.size_hint or (None, None)
shmn_w, shmn_h = child.size_hint_min or (None, None)
shmx_w, shmx_h = child.size_hint_max or (None, None)

if sh_w is not None:
new_child_rect = new_child_rect.resize(width=self.content_width * sh_w)

if shmn_w:
new_child_rect = new_child_rect.min_size(width=shmn_w)
if shmx_w:
new_child_rect = new_child_rect.max_size(width=shmx_w)

if sh_h is not None:
new_child_rect = new_child_rect.resize(height=self.content_height * sh_h)

if shmn_h:
new_child_rect = new_child_rect.min_size(height=shmn_h)
if shmx_h:
new_child_rect = new_child_rect.max_size(height=shmx_h)

child.rect = new_child_rect

def on_update(self, dt):
self._time += dt


class MyView(UIView):
def __init__(self):
super().__init__()
self.background_color = arcade.uicolor.BLUE_BELIZE_HOLE

root = self.ui.add(UIAnchorLayout())

# Create a custom layout
self.circle_layout = root.add(CircleLayout(size_hint=(0.8, 0.8)))

# Add buttons to the layout
for i in range(8):
self.circle_layout.add(
UIFlatButton(
text=f"Button {i}",
size_hint=(0.1, 0.1),
)
)


if __name__ == "__main__":
window = arcade.Window(title="GUI Example: CircleLayout")
window.show_view(MyView())
arcade.run()
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
Read more about properties in the `arcade.gui` documentation.
If arcade and Python are properly installed, you can run this example with:
python -m arcade.examples.gui.own_progressbar
python -m arcade.examples.gui.own_widgets
"""

from __future__ import annotations
Expand Down Expand Up @@ -127,7 +127,7 @@ def _update_bar(self):
class MyView(UIView):
def __init__(self):
super().__init__()
self.ui = arcade.gui.UIManager()
self.background_color = arcade.uicolor.BLUE_BELIZE_HOLE

root = self.ui.add(UIAnchorLayout())
bars = root.add(UIBoxLayout(space_between=10))
Expand Down
Binary file added doc/example_code/images/gui_own_layout.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/example_code/images/gui_own_widgets.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions doc/example_code/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,18 @@ Graphical User Interface

:ref:`gui_6_size_hints`

.. figure:: images/thumbs/gui_own_widgets.png
:figwidth: 170px
:target: gui_own_widgets.html

:ref:`gui_own_widgets`

.. figure:: images/thumbs/gui_own_layout.png
:figwidth: 170px
:target: gui_own_layout.html

:ref:`gui_own_layout`

.. note::

Not all existing examples made it into this section. You can find more under `Arcade GUI Examples <https://github.com/pythonarcade/arcade/tree/development/arcade/examples/gui>`_
Expand Down
25 changes: 16 additions & 9 deletions doc/programming_guide/gui/concepts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ A :class:`UIWidget` has following properties.
move or resize its children; use a :py:class:`~arcade.gui.UILayout`
instead.

``visible``
A boolean indicating if the widget is visible or not. If a widget is not
visible, itself and any child widget will not be rendered.
Especially useful for hiding parts of the GUI like dialogs or popups.

``size_hint``
A tuple of two normalized floats (``0.0``-``1.0``) describing the portion
of the parent's width and height this widget prefers to occupy.
Expand Down Expand Up @@ -237,9 +242,9 @@ behaviour. Currently the available Mixins are still under heavy development.

Available:

- :py:class:`UIDraggableMixin`
- :py:class:`UIMouseFilterMixin`
- :py:class:`UIWindowLikeMixin`
- :py:class:`UIDraggableMixin` - Makes a widget draggable with the mouse.
- :py:class:`UIMouseFilterMixin` - Captures all mouse events.
- :py:class:`UIWindowLikeMixin` - Makes a widget behave like a window, combining draggable and mouse filter behaviour.

UIConstructs
============
Expand All @@ -248,8 +253,8 @@ Constructs are predefined structures of widgets and layouts like a message box.

Available:

- :py:class:`UIMessageBox`
- :py:class:`UIButtonRow`
- :py:class:`UIMessageBox` - A simple message box with a title, message and buttons.
- :py:class:`UIButtonRow` - A row of buttons.

Available Elements
==================
Expand Down Expand Up @@ -511,8 +516,8 @@ game developer should mostly interact with user-interface events, which are
dispatched from specific :py:class:`~arcade.gui.UIWidget`s like an ``on_click``
of a button.

In rare cases a developer might implement some widgets themselves or want to
modify the existing GUI behavior. In those cases a developer might register own
In cases where a developer implement own widgets themselves or want to
modify the existing GUI behavior, the developer might register own
pyglet event types on widgets or overwrite the
:py:class:`~arcade.gui.UIWidget.on_event` method. In that case, refer to
existing widgets as an example.
Expand Down Expand Up @@ -552,6 +557,8 @@ events.
Property
````````

:py:class:`~arcade.gui.Property` is an pure-Python implementation of Kivy
like Properties. They are used to detect attribute changes of widgets and trigger
:py:class:`~arcade.gui.Property` is an pure-Python implementation of Kivy-like Properties.
They are used to detect attribute changes of widgets and especially to trigger
rendering. They are mostly used within GUI widgets, but are globally available since 3.0.0.

Properties are a less verbose way to implement the observer pattern compared to the property decorator.
1 change: 1 addition & 0 deletions doc/programming_guide/gui/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Find the required information in the following sections:
layouts
style
own_widgets
own_layout



40 changes: 40 additions & 0 deletions doc/programming_guide/gui/own_layout.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
.. _gui_own_layout:

Own Layout
----------

Creating own layouts is the master class of creating own widgets.
It allows you to create custom layouts that can be used in your application to position widgets.

General use cases for own layouts are:

- Create a layout that positions widgets in a specific way, like in a circle.
- Animate widgets in a specific way, like a carousel.

Where to start
~~~~~~~~~~~~~~

To create own layout, you need to create a new class that inherits from :class:`arcade.gui.UILayout`.

The main method you need to implement is:

- :meth:`arcade.gui.UILayout.do_layout` - This method is called to layout the child widgets.

Widgets added to the layout are accessible via the :attr:`arcade.gui.UILayout._children` attribute,
which is a list of all added widgets with the parameter provided when added.

Children should be placed within the bounds of the layout.
And should respect size_hint, size_hint_min and size_hint_max of the children.


It also provides a great user experience when you provide custom docs for the :meth:`arcade.gui.UIWidget.add` method.
So the user knows how to add widgets to your layout and which parameter are supported.

In the following example, we will create a layout that positions widgets in a circle and slowly rotating them.

Example `CircleLayout`
~~~~~~~~~~~~~~~~~~~~~~

.. literalinclude:: ../../../arcade/examples/gui/own_layout.py


18 changes: 10 additions & 8 deletions doc/programming_guide/gui/own_widgets.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,22 @@ Where to start

To create own widgets, you need to create a new class that inherits from :class:`arcade.gui.UIWidget`.

While inheriting from :class:`arcade.gui.UIWidget`, provides the highest flexibility,
you can also make use of other base classes, which provide a more specialized interface.
While inheriting from :class:`arcade.gui.UIWidget`, provides the highest flexibility.
The main methods you need to implement are:
- :meth:`arcade.gui.UIWidget.do_render` - This method is called to render the widget.
- :meth:`arcade.gui.UIWidget.on_event` - This method is called to handle events like mouse or keyboard input.
- :meth:`arcade.gui.UIWidget.on_update` - This method is called to update the widget (same frequency like window).

You can also make use of other base classes, which provide a more specialized interface.
Further baseclasses are:

- :class:`arcade.gui.UIInteractiveWidget`
`UIInteractiveWidget` is a baseclass for widgets that can be interacted with.
It provides a way to handle mouse events and properties like `hovered` or `pressed`.
In addition it already implements the `on_click` method,
which can be used to react to a click event.
It handles mouse events and provides properties like `hovered` or `pressed` and an :meth:`on_click` method.

- :class:`arcade.gui.UIAnchorLayout`
`UIAnchorLayout` is basically a frame, which can be used to position widgets
to a place within the widget. This makes it a great baseclass for a widget containing
`UIAnchorLayout` is basically a frame, which can be used to place widgets
to a position within itself. This makes it a great baseclass for a widget containing
multiple other widgets. (Examples: `MessageBox`, `Card`, etc.)

If your widget should act more as a general layout, position various widgets and handle their size,
Expand All @@ -43,6 +45,6 @@ to show the differences between two of the base classes.
Example `ProgressBar`
~~~~~~~~~~~~~~~~~~~~~

.. literalinclude:: ../../../arcade/examples/gui/own_progressbar.py
.. literalinclude:: ../../../arcade/examples/gui/own_widget.py


0 comments on commit 4dbaacf

Please sign in to comment.