From 10ee9f907026f4a38f1d4078a943ac9f2bc717ef Mon Sep 17 00:00:00 2001 From: Ethan Chan <103769713+eschan145@users.noreply.github.com> Date: Tue, 4 Apr 2023 16:08:10 -0700 Subject: [PATCH 1/9] Improve layout and formatting of `concept.rst` for GUI system docs Added a lot of documentation and a good tutorial on the GUI concepts. Also added more reStructuredText formatting. --- doc/programming_guide/gui/concept.rst | 458 +++++++++++++++++++------- 1 file changed, 348 insertions(+), 110 deletions(-) diff --git a/doc/programming_guide/gui/concept.rst b/doc/programming_guide/gui/concept.rst index 91148f829f..fcf0373795 100644 --- a/doc/programming_guide/gui/concept.rst +++ b/doc/programming_guide/gui/concept.rst @@ -3,79 +3,99 @@ GUI Concepts ------------ -GUI elements are represented as instances of :class:`UIWidget`. The GUI is structured like a tree, every widget -can have other widgets as children. +GUI elements are represented as instances of :py:class:`~arcade.gui.UIWidget`. +The GUI is structured like a tree; every widget can have other widgets as +children. -The root of the tree is the :class:`UIManager`. The UIManager connects the user interactions with the GUI. -Read more about :ref:`UIEvent`. +The root of the tree is the :py:class:`~arcade.gui.UIManager`. The +:class:`UIManager` connects the user interactions with the GUI. Read more about +:ref:`UIEvent`. -Classes of Arcades GUI code are prefixed with UI- to make them easy to identify and search for in autocompletion. +Classes of arcade's GUI code are prefixed with ``UI-`` to make them easy to +identify and search for in autocompletion. UIWidget ======== -:class:`UIWidget` are the core of Arcades GUI. A widget represents the behaviour and graphical -representation of any element (like Buttons or Text) +A :py:class:`~arcade.class.UIWidget` is the core of arcade's GUI. A widget +represents the behaviour and graphical representation of any element (like +buttons or text). -A :class:`UIWidget` has following properties +A :class:`UIWidget` has following properties. -**rect** - x and y coordinates (bottom left of the widget), width and height +``rect`` + A tuple with four slots. The first two are x and y coordinates (bottom + left of the widget), and the last two are width and height. -**children** - Child widgets, rendered within this widget - A :class:`UIWidget` will not move or resize its children, use a :class:`UILayout` instead. +``children`` + Child widgets rendered within this widget. A :class:`UIWidget` will not + move or resize its children; use a :py:class:`~arcade.gui.UILayout` + instead. -**size_hint** - tuple of two floats, defines how much of the parents space it would like to occupy (range: 0.0-1.0). - For maximal vertical and horizontal expansion, define `size_hint` of 1 for the axis. +``size_hint`` + A tuple of two floats. Defines how much of the parent's space it would like + to occupy (range: ``0.0-1.0``). For maximal vertical and horizontal + expansion, define a ``size_hint`` of 1 for the axis. -**size_hint_min** - tuple of two ints, defines minimal size of the widget. - If set, changing the size of a widget to a lower values will use this size instead. +``size_hint_min`` + A tuple of two integers. Defines minimal size of the widget. Changing the + size of a widget to a lower values will use this size instead. -**size_hint_max** - tuple of two ints, defines maximum size of the widget. - If set, changing the size of a widget to a higher values will use this size instead. +``size_hint_max`` + A tuple of two integers. Defines maximum size of the widget. Changing the + size of a widget to a higher values will use this size instead. - *size_hint*, *size_hint_min*, and *size_hint_max* are values that are additional information of a widget, but do not - effect the widget on its own. :class:`UILayout` may use these information to place or resize a widget. +``size_hint``, ``size_hint_min``, and ``size_hint_max`` are values that are +additional information of a widget, but do not effect the widget on its own. +:class:`UILayout` may use this data to place or resize a widget. Rendering -......... +````````` -:meth:`UIWidget.do_render` is called recursively if rendering was requested via :meth:`UIWidget.trigger_render`. -In case widgets have to request their parents to render use :meth:`UIWidget.trigger_full_render` +:py:meth:`~arcade.gui.UIWidget.do_render` is called recursively if rendering +was requested via :py:meth:`~arcade.gui.UIWidget.trigger_render`. In case +widgets have to request their parents to render, use +:py:meth:`arcade.gui.UIWidget.trigger_full_render`. -The widget has to draw itself and child widgets within :meth:`UIWidget.do_render`. Due to the deferred functionality -render does not have to check any dirty variables, as long as state changes use the trigger function. +The widget has to draw itself and child widgets within +:py:meth:`~arcade.gui.UIWidget.do_render`. Due to the deferred functionality +render does not have to check any dirty variables, as long as state changes use +the :py:meth:`~arcade.gui.UIWidget.trigger_full_render` method. -For widgets, that might have transparent areas, they have to request a full rendering. +For widgets, that might have transparent areas, they have to request a full +rendering. + +.. warning:: Enforced rendering of the whole GUI might be very expensive! UILayout ======== -:class:`UILayout` are widgets, which reserve the option to move or resize children. They might respect special properties -of a widget like *size_hint*, *size_hint_min*, or *size_hint_max*. +:py:class:`~arcade.gui.UILayout` are widgets, which reserve the option to move +or resize children. They might respect special properties of a widget like +``size_hint``, ``size_hint_min``, or ``size_hint_max``. -The :class:`UILayout` only resize a child's dimension (x or y axis) if size_hint provides a value for the axis, which is not `None` for the dimension. +The :py:class:`arcade.gui.UILayout` only resizes a child's dimension (x or y +axis) if ``size_hint`` provides a value for the axis, which is not ``None`` for +the dimension. Algorithm -......... +````````` -:class:`UIManager` triggers the layout and render process right before the actual frame draw. -This opens the possibility, to adjust to multiple changes only ones. +:py:class:`arcade.gui.UIManager` triggers the layout and render process right +before the actual frame draw. This opens the possibility to adjust to multiple +changes only once. -Example: Executed steps within :class:`UIBoxLayout`: +**Example**: Executed steps within :py:class:`~arcade.gui.UIBoxLayout`: -1. :meth:`UIBoxLayout.do_layout` - 1. collect current size, size_hint, size_hint_min of children - 2. calculate the new position and sizes - 3. set position and size of children -2. recursive call `do_layout` on child layouts (last step in :meth:`UIBoxLayout.do_layout`) +1. :py:meth:`~arcade.UIBoxLayout.do_layout` + 1. Collect current ``size``, ``size_hint``, ``size_hint_min`` of children + 2. Calculate the new position and sizes + 3. Set position and size of children +2. Recursively call ``do_layout`` on child layouts (last step in + :py:meth:`~arcade.gui.UIBoxLayout.do_layout`) .. code-block:: @@ -110,7 +130,7 @@ Example: Executed steps within :class:`UIBoxLayout`: └─────────┘ └────────┘ └────────┘ Size hint support -+++++++++++++++++ +^^^^^^^^^^^^^^^^^ +--------------------------+------------+----------------+----------------+ | | size_hint | size_hint_min | size_hint_max | @@ -127,99 +147,317 @@ Size hint support UIMixin ======= -Mixin classes are a base class which can be used to apply some specific behaviour. Currently the available Mixins are -still under heavy development. +Mixin classes are a base class which can be used to apply some specific +behaviour. Currently the available Mixins are still under heavy development. Constructs ========== -Constructs are predefined structures of widgets and layouts like a message box or (not yet available) file dialogues. +Constructs are predefined structures of widgets and layouts like a message box +or (not yet available) file dialogues. Available Elements ================== -- :class:`UIWidget`: - - :class:`UIFlatButton` - 2D flat button for simple interactions (hover, press, release, click) - - :class:`UITextureButton` - textured button (use :meth:`arcade.load_texture()`) for simple interactions (hover, press, release, click) - - :class:`UILabel` - Simple text, supports multiline, fits content - - :class:`UIInputText` - field to accept user text input - - :class:`UITextArea` - Multiline scrollable text widget. - - :class:`UISpriteWidget` - Embeds a Sprite within the GUI tree -- :class:`UILayout`: - - :class:`UIBoxLayout` - Places widgets next to each other (vertical or horizontal) - - :class:`UIAnchorLayout` - Places widgets within itself following anchor information - - :class:`UIGridLayout` - Places widgets within a grid -- Constructs - - :class:`UIMessageBox` - Popup box with a message text and a few buttons. -- Mixins - - :class:`UIDraggableMixin` - Makes a widget draggable. - - :class:`UIMouseFilterMixin` - Catches mouse events that occure within the widget boundaries. - - :class:`UIWindowLikeMixin` - Combination of :class:`UIDraggableMixin` and :class:`UIMouseFilterMixin`. +Buttons +``````` + +As with most widgets, buttons take ``x``, ``y``, ``width``, and ``height`` +parameters for their sizing. Buttons specifically have two more parameters - +``text`` and ``multiline``. + +All button types support styling. And they are text widgets, which means you +can use the :py:attr:`~arcade.gui.UITextWidget._label` attribute to get the +label component of the button. + +Flat button +^^^^^^^^^^^ + +**Name**: :py:class:`~arcade.gui.FlatButton` + +A flat button for simple interactions (hover, press, release, click). This +button is created with a simple rectangle. Flat buttons can quickly create a +nice-looking button. However, depending on your use case, you may want to use +a texture button to further customize your look and feel. + +Styling options are shown in the table below. + ++----------------+------------------------------------------------------------+ +|Name |Description | ++================+============================================================+ +|``font_size`` |Font size for the button text. Defaults to 12. | ++----------------+------------------------------------------------------------+ +|``font_name`` |Font name or family for the button text. If a tuple is | +| |supplied then arcade will attempt to load all of the fonts, | +| |prioritizing the first one. Defaults to | +| |``("calibri", "arial")``. | ++----------------+------------------------------------------------------------+ +|``font_color`` |Font color for the button text (foreground). Defaults to | +| |white for normal, hover, and disabled states. Defaults to | +| |black for pressed state. | ++----------------+------------------------------------------------------------+ +|``bg`` |Background color of the button. This modifies the color of | +| |the rectangle within the button and not the border. Instead | +| |of making each of these different colors for each of your | +| |buttons, set these towards a common color theme. Defaults to| +| |gray for hover and disabled states. Otherwise it is white. | ++----------------+------------------------------------------------------------+ +|``border`` |Border color. It is common to only modify this in a focus or| +| |hover state. Defaults to white or turquoise for hover. | ++----------------+------------------------------------------------------------+ +|``border_width``|Width of the border/outline of the button. It is common to | +| |make this thicker on a hover or focus state, however an | +| |overly thick border will result in your GUI looking old or | +| |low-quality. Defaults to 2. | ++----------------+------------------------------------------------------------+ + +Image/texture button +^^^^^^^^^^^^^^^^^^^^ + +**Name**: :py:class:`~arcade.gui.UITextureButton` + +An image button. Textures are supplied from :py:func:`arcade.load_texture` for +simple interactions (hover, press, release, click). A texture lets you further +customize the look of the widget better than styling. + +A texture button a few more arguments than a flat button. ``texture``, +``texture_hovered``, and ``texture_pressed`` will change the texture displayed +on the button respectively. ``scale`` will change the scaling or size of the +button - it's similar to the sprite :py:attr:`~arcade.Sprite.scale`. + +.. hint:: + This widget *does* have ``width`` and ``height`` parameters, but they only + stretch the texture instead of resizing it with keeping the borders. This + feature is currently in-progress. + +Texture buttons have fewer styling options when they have a texture compared to +flat buttons. + ++----------------+------------------------------------------------------------+ +|Name |Description | ++================+============================================================+ +|``font_size`` |Font size for the button text. Defaults to 12. | ++----------------+------------------------------------------------------------+ +|``font_name`` |Font name or family for the button text. If a tuple is | +| |supplied then arcade will attempt to load all of the fonts, | +| |prioritizing the first one. Defaults to | +| |``("calibri", "arial")``. | ++----------------+------------------------------------------------------------+ +|``font_color`` |Font color for the button text (foreground). Defaults to | +| |white for normal, hover, and disabled states. Defaults to | +| |black for pressed state. | ++----------------+------------------------------------------------------------+ +|``border_width``|Width of the border/outline of the button. It is common to | +| |make this thicker on a hover or focus state, however an | +| |overly thick border will result in your GUI looking old or | +| |low-quality. Defaults to 2. | ++----------------+------------------------------------------------------------+ + +Text widgets +```````````` + +All text widgets take ``x`` and ``y`` positioning parameters. They also accept +``text`` and ``multiline`` options. + +Label +^^^^^ + +**Name**: :py:class:`~arcade.gui.UILabel` + +A label is used to display text as instruction for the user. Multiline text is +supported, and what would have been its style options were moved into the +parameters. + +This widget has no style options whatsoever, and they have been moved into the +parameters. ``bold`` and ``italic`` will set the text to bold or italic. +``align`` specifies the justification of the text. Additionally it takes +``font_name``, ``font_size``, and ``text_color`` options. + +Using the :py:attr:`~arcade.gui.UILabel.label` property accesses the internal +:py:class:`~arcade.Text` class. + +.. hint:: + A :py:attr:`~arcade.gui.UILabel.text` attribute can modify the displayed + text. Beware-calling this again and again will give a lot of lag. Use + :py:meth:`~arcade.Text.begin_update` and py:meth:`~arcade.Text.end_update` + to speed things up. + +Text input field +^^^^^^^^^^^^^^^^ + +**Name**: :py:class:`~arcade.gui.UIInputText` + +A text field allows a user to input a basic string. It uses pyglet's +:py:class:`~pyglet.text.layout.IncrementalTextLayout` and its +:py:class:`~pyglet.text.caret.Caret`. These are stored in ``layout`` and +``caret`` properties. + +This widget takes ``width`` and ``height`` properties and uses a rectangle to +display a background behind the layout. + +A text input field allows the user to move a caret around text to modify it, as +well as selecting parts of text to replace or delete it. Motion symbols for a +text field are listed in :py:mod:`pyglet.window.key` module. + +Text area +^^^^^^^^^ + +**Name**: :py:class:`~arcade.gui.UITextArea` + +A text area is a scrollable text widget. A user can scroll the mouse to view a +rendered text document. **This does not support editing text**. Think of it as +a scrollable label instead of a text field. + +``width`` and ``height`` allocate a size for the text area. If text does not +fit within these dimensions then only part of it will be displayed. Scrolling +the mouse will display other sections of the text incrementally. Other +parameters include ``multiline`` and ``scroll_speed``. See +:py:attr:`~pyglet.text.layout.ScrollableTextLayout.view_y` on scroll speed. + +Use ``layout`` and ``doc`` to get the pyglet layout and document for the +text area, respectively. .. _UIEvent: -UIEvents -======== - -UIEvents are fully typed dataclasses, which provide information about a event effecting the UI. - -All pyglet window events are converted by the UIManager into UIEvents and passed via dispatch_event -to the ``on_event`` callbacks. +User-interface events +===================== + +Arcade's GUI events are fully typed dataclasses, which provide information +about a event effecting the UI. + +All pyglet window events are converted by the +:py:class:`~arcade.gui.UIManager` into :class:`UIEvents` and passed via +:py:meth:`~pyglet.event.EventDispatcher.dispatch_event` to the +:py:meth:`~arcade.gui.UIWidget.on_event` callbacks. + +Widget-specific events (such as :py:class:`~arcade.gui.UIOnClickEvent` are +dispatched via ``on_event`` and are then dispatched as specific event types +(like ``on_click``). + +A full list of event attributes is shown below. + ++---------------------------+-----------------------------------------+ +|Event |Attributes | ++===========================+=========================================+ +|``UIEvent`` |None | ++---------------------------+-----------------------------------------+ +|``UIMouseEvent`` |``x``, ``y`` | ++---------------------------+-----------------------------------------+ +|``UIMouseMovementEvent`` |``dx``, ``dy`` | ++---------------------------+-----------------------------------------+ +|``UIMousePressEvent`` |``dx``, ``dy``, ``button``, ``modifiers``| ++---------------------------+-----------------------------------------+ +|``UIMouseDragEvent`` |``dx``, ``dy`` | ++---------------------------+-----------------------------------------+ +|``UIMouseScrollEvent`` |``scroll_x``, ``scroll_y`` | ++---------------------------+-----------------------------------------+ +|``UIKeyEvent`` |``symbol``, ``modifiers`` | ++---------------------------+-----------------------------------------+ +|``UIKeyReleaseEvent`` |None | ++---------------------------+-----------------------------------------+ +|``UITextEvent`` |``text`` | ++---------------------------+-----------------------------------------+ +|``UITextMotionEvent`` |``motion`` | ++---------------------------+-----------------------------------------+ +|``UITextMotionSelectEvent``|``selection`` | ++---------------------------+-----------------------------------------+ +|``UIOnClickEvent`` |None | ++---------------------------+-----------------------------------------+ +|``UIOnUpdateEvent`` |``dt`` | ++---------------------------+-----------------------------------------+ +|``UIOnChangeEvent`` |``old_value``, ``new_value`` | ++---------------------------+-----------------------------------------+ +|``UIOnActionEvent`` |``action`` | ++---------------------------+-----------------------------------------+ + +- :py:class:`arcade.gui.UIEvent`. Base class for all events. +- :py:class:`arcade.gui.UIMouseEvent`. Base class for mouse-related events. + - :py:class:`arcade.gui.UIMouseMovementEvent`. Mouse motion. This event + has an additional ``pos`` property that returns a tuple of the x and y + coordinates. + - :py:class:`~arcade.gui.UIMousePressEvent`. Mouse button pressed. + - :py:class:`~arcade.gui.UIMouseDragEvent`. Mouse pressed and moved (drag). + - :py:class:`~arcade.gui.UIMouseReleaseEvent`. Mouse button release. + - :py:class:`~arcade.gui.UIMouseScrollEvent`. Mouse scroll. +- :py:class:`~arcade.gui.UITextEvent`. Text input from user. This is only used + for text fields and is the text as a string that was inputed. +- :py:class:`~arcade.gui.UITextMotionEvent`. Text motion events. This includes + moving the text around with the caret. Examples include using the arrow + keys, backspace, delete, or any of the home/end and PgUp/PgDn keys. Holding + ``Control`` with an arrow key shifts the caret by a entire word or paragraph. + Moving the caret via the mouse does not trigger this event. +- :py:class:`~arcade.gui.UITextMotionSelectEvent`. Text motion events for + selection. Holding down the ``Shift`` key and pressing arrow keys + (``Control`` optional) will select character(s). Additionally, using a + ``Control-A`` keyboard combination will select all text. Selecting text via + the mouse does not trigger this event. +- :py:class:`~arcade.gui.UIOnUpdateEvent`. This is a callback to the arcade + :py:class:`~arcade.Window.on_update` method. + +Widget-specific events +`````````````````````` + +Widget events are only dispatched as a pyglet event on a widget itself and are +not passed through the widget tree. + +- :py:class:`~arcade.gui.UIOnClickEvent`. Click event of + :py:class:`~arcade.gui.UIInteractiveWidget` class. This is triggered on + widget press. +- :py:class:`~arcade.gui.UIOnChangeEvent`. A value of a + :py:class:`~arcade.gui.UIWidget` has changed. +- :py:class:`~arcade.gui.UIOnActionEvent`. An action results from interaction + with the :py:class:`~arcade.gui.UIWidget` (mostly used in constructs) + +Different event systems +======================= -Widget specific UIEvents like UIOnClick are dispatched via "on_event" and are then dispatched as specific event types (like 'on_click') +Arcade's GUI uses different event systems, dependent on the required flow. A +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. -- :class:`UIEvent` - Base class for all events -- :class:`UIMouseEvent` - Base class for mouse related event - - :class:`UIMouseMovementEvent` - Mouse moves - - :class:`UIMousePressEvent` - Mouse button pressed - - :class:`UIMouseDragEvent` - Mouse pressed and moved (drag) - - :class:`UIMouseReleaseEvent` - Mouse button released - - :class:`UIMouseScrollEvent` - Mouse scolls -- :class:`UITextEvent` - Text input from user -- :class:`UITextMotionEvent` - Text motion events like arrows -- :class:`UITextMotionSelectEvent` - Text motion events for selection -- :class:`UIOnUpdateEvent` - arcade.Window `on_update` callback +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 +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. -Widget specific events -...................... +Pyglet window events +```````````````````` -Widget events are only dispatched as a Pyglet event on a widget itself and are not passed through the widget tree. +Pyglet window events are received by :py:class:`~arcade.gui.UIManager`. -- :class:`UIOnClickEvent` - Click event of :class:`UIInteractiveWidget` class -- :class:`UIOnChangeEvent` - A value of a :class:`UIWidget` has changed -- :class:`UIOnActionEvent` - An action results from interaction with the :class:`UIWidget` (mostly used in constructs) +You can dispatch them via:: -Different Event Systems -======================= + UIWidget.dispatch_event("on_event", UIEvent(...)) -The GUI uses different event systems, dependent on the required flow. A game developer should mostly interact with UIEvents -which are dispatched from specific UIWidgets like ``on_click`` of a button. +Window events are wrapped into subclasses of :py:class:`~arcade.gui.UIEvent`. -In rare cases a developer might implement some UIWidgets or wants to modify the existing GUI behavior. In those cases a -developer might register own Pyglet event types on UIWidgets or overwrite the ``UIWidget.on_event`` method. +Pyglet event dispatcher - UIWidget +`````````````````````````````````` -Pyglet Window Events -.................... +Widgets implement pyglet's :py:class:`~pyglet.event.EventDispatcher` and +register an ``on_event`` event type. -Received by UIManager, dispatched via ``UIWidget.dispatch_event("on_event", UIEvent(...))``. -Window Events are wrapped into subclasses of UIEvent. +:py:meth:`~arcade.gui.UIWidget.on_event` contains specific event handling and +should not be overwritten without deeper understanding of the consequences. -Pyglet EventDispatcher - UIWidget -................................. +To add custom event handling, use the decorator syntax to add another +listener:: -UIWidgets implement Pyglets EventDispatcher and register an ``on_event`` event type. -``UIWidget.on_event`` contains specific event handling and should not be overwritten without deeper understanding of the consequences. -To add custom event handling use the decorator syntax to add another listener (``@UIWidget.event("on_event")``). + @UIWidget.event("on_event") -UIEvents -........ +User-interface events +````````````````````` -UIEvents are typed representations of events that are passed within the GUI. UIWidgets might define and dispatch their own subclasses of UIEvents. +User-interface events are typed representations of events that are passed +within the GUI. Widgets might define and dispatch their own subclasses of these +events. Property -........ - -``Property`` is an pure-Python implementation of Kivy Properties. They are used to detect attribute -changes of UIWidgets and trigger rendering. They should only be used in arcade internal code. +```````` +:py:class:`~arcade.gui.Property` is an pure-Python implementation of Kivy +Properties. They are used to detect attribute changes of widgets and trigger +rendering. They should only be used in arcade internal code. From b034b12eeb0c8760a9e1347b5f1ff2e6b5263185 Mon Sep 17 00:00:00 2001 From: Ethan Chan <103769713+eschan145@users.noreply.github.com> Date: Tue, 4 Apr 2023 19:55:00 -0700 Subject: [PATCH 2/9] Fix typo of "a" to "an" and "effecting" to "affecting" Thanks @pushfoo! Co-authored-by: Paul <36696816+pushfoo@users.noreply.github.com> --- doc/programming_guide/gui/concept.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/programming_guide/gui/concept.rst b/doc/programming_guide/gui/concept.rst index fcf0373795..ac3fd1cc17 100644 --- a/doc/programming_guide/gui/concept.rst +++ b/doc/programming_guide/gui/concept.rst @@ -324,7 +324,7 @@ User-interface events ===================== Arcade's GUI events are fully typed dataclasses, which provide information -about a event effecting the UI. +about an event affecting the UI. All pyglet window events are converted by the :py:class:`~arcade.gui.UIManager` into :class:`UIEvents` and passed via From b14866eae5cc487e2e6d43cc97be9a67a64f452f Mon Sep 17 00:00:00 2001 From: Ethan Chan <103769713+eschan145@users.noreply.github.com> Date: Tue, 4 Apr 2023 20:04:59 -0700 Subject: [PATCH 3/9] Apply suggestions to `concept.rst` by pushfoo * Wrapped lines to eighty characters --- doc/programming_guide/gui/concept.rst | 38 ++++++++++++++++++--------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/doc/programming_guide/gui/concept.rst b/doc/programming_guide/gui/concept.rst index ac3fd1cc17..91a31958d7 100644 --- a/doc/programming_guide/gui/concept.rst +++ b/doc/programming_guide/gui/concept.rst @@ -17,9 +17,9 @@ identify and search for in autocompletion. UIWidget ======== -A :py:class:`~arcade.class.UIWidget` is the core of arcade's GUI. A widget -represents the behaviour and graphical representation of any element (like -buttons or text). +The :py:class:`~arcade.gui.UIWidget` class is the core of arcade's GUI system. +Widgets specify the behavior and graphical representation of any UI element, +such as buttons or labels. A :class:`UIWidget` has following properties. @@ -33,21 +33,33 @@ A :class:`UIWidget` has following properties. instead. ``size_hint`` - A tuple of two floats. Defines how much of the parent's space it would like - to occupy (range: ``0.0-1.0``). For maximal vertical and horizontal - expansion, define a ``size_hint`` of 1 for the axis. + 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. + + Examples:: + + # Prefer to take up all space within the parent + widget.size_hint = (1.0, 1.0) + + # Prefer to take up the full width & half the height of the parent + widget.size_hint = (1.0, 0.5) + # Prefer using 1/10th of the available width & height + widget.size_hint = (0.1, 0.1) ``size_hint_min`` - A tuple of two integers. Defines minimal size of the widget. Changing the - size of a widget to a lower values will use this size instead. + A tuple of two integers defining the minimum width and height of the + widget. Attempting to set a smaller width or height on the widget will fail + by defaulting to the minimum values specified here. ``size_hint_max`` - A tuple of two integers. Defines maximum size of the widget. Changing the - size of a widget to a higher values will use this size instead. + A tuple of two integers defining the maximum width and height of the + widget. Attempting to set a larger width or height greater will fail by + defaulting to the to the maximum values specified here. -``size_hint``, ``size_hint_min``, and ``size_hint_max`` are values that are -additional information of a widget, but do not effect the widget on its own. -:class:`UILayout` may use this data to place or resize a widget. +.. warning:: Size hints do nothing on their own! + + They are hints to :class:`UILayout` instances, which may choose to use or + ignore them. Rendering ````````` From 1247831e6e113d25ea9607698931da95ecf33894 Mon Sep 17 00:00:00 2001 From: Ethan Chan Date: Thu, 6 Apr 2023 21:02:42 -0700 Subject: [PATCH 4/9] Documentation improvements to GUI text widgets I intend to add some more commits later. I have to say, though, this traditional RST parameter formatting looks really ugly and I may need to squish text into very small areas. I additionally added a `caret_color` parameter to `UIInputText` allowing users to specify a color for the caret. --- arcade/gui/widgets/text.py | 217 +++++++++++++++++++++++-------------- 1 file changed, 137 insertions(+), 80 deletions(-) diff --git a/arcade/gui/widgets/text.py b/arcade/gui/widgets/text.py index b7dca3433b..2fcdb3b7ba 100644 --- a/arcade/gui/widgets/text.py +++ b/arcade/gui/widgets/text.py @@ -23,33 +23,47 @@ class UILabel(UIWidget): - """A simple text label. Also supports multiline text. - In case you want to scroll text use a :class:`UITextArea` - By default a :class:`UILabel` will fit its initial content, - if the text changed use :meth:`UILabel.fit_content` to adjust the size. - - :param float x: x coordinate of bottom left - :param float y: y coordinate of bottom left - :param float width: width of widget. Defaults to text width if not specified. - :param float height: height of widget. Defaults to text height if not specified. - :param str text: text of the label. - :param font_name: a list of fonts to use. Program will start at the beginning of the list - and keep trying to load fonts until success. - :param float font_size: size of font. - :param RGBA255 text_color: Color of font. - :param bool bold: Bold font style. - :param bool italic: Italic font style. + """A simple text label. This widget is meant to display user instructions or + information. This label supports multiline text. + + If you want to make a scrollable viewing text box, use a + :py:class:`~arcade.gui.UITextArea`. + + By default, a label will fit its initial content. If the text is changed use + :py:meth:`~arcade.gui.UILabel.fit_content` to adjust the size. + + :param float x: x position (default anchor is bottom-left). + :param float y: y position (default anchor is bottom-left). + :param float width: Width of the label. Defaults to text width if not + specified. See + :py:meth:`~pyglet.text.layout.TextLayout.content_width`. + :param float height: Height of the label. Defaults to text height if not + specified. See + :py:meth:`~pyglet.text.layout.TextLayout.content_height`. + :param str text: Text displayed on the label. + :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 float font_size: Font size of font. + :param RGBA255 text_color: Color of the text. + :param bool bold: If enabled, the label's text will be in a **bold** style. + :param bool italic: If enabled, the label's text will be in an *italic* + style. :param bool stretch: Stretch font style. - :param str align: Horizontal alignment of text on a line, only applies if a width is supplied. - One of ``"left"``, ``"center"`` or ``"right"``. - :param float dpi: Resolution of the fonts in this layout. Defaults to 96. - :param bool multiline: if multiline is true, a \\n will start a new line. - A UITextWidget with multiline of true is the same thing as UITextArea. - - :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. + :param str align: Horizontal alignment of text on a line. This only applies + if a width is supplied. Valid options include ``"left"``, + ``"center"`` or ``"right"``. + :param float dpi: Resolution of the fonts in the layout. Defaults to 96. + :param bool multiline: If enabled, a ``\\n`` will start a new line. A + :py:class:`~arcade.gui.UITextWidget` with + ``multiline`` of True is the same thing as + a :py:class:`~arcade.gui.UITextArea`. + + :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: Not used. Labels will have no need for a style; they are too + simple (just a text display). """ def __init__( @@ -71,7 +85,7 @@ def __init__( size_hint_max=None, **kwargs, ): - # Use Arcade wrapper of pyglet.Label for text rendering + # Use Arcade Text wrapper of pyglet.Label for text rendering self.label = arcade.Text( start_x=0, start_y=0, @@ -83,10 +97,11 @@ def __init__( bold=bold, italic=italic, align=align, - anchor_y="bottom", # position text bottom left, to fit into scissor box - multiline=multiline, + anchor_y="bottom", # Position text bottom left to fit into scissor + multiline=multiline, # area **kwargs, ) + super().__init__( x=x, y=y, @@ -98,9 +113,9 @@ def __init__( **kwargs, ) - # set label size, if the width or height was given - # because border and padding can only be applied later, we can avoid `fit_content()` - # and set with and height separately + # Set the label size. If the width or height was given because border + # and padding can only be applied later, we can avoid ``fit_content`` + # and set with and height separately. if width: self.label.width = int(width) if height: @@ -110,7 +125,7 @@ def __init__( def fit_content(self): """ - Sets the width and height of this UIWidget to contain the whole text. + Set the width and height of the label to contain the whole text. """ base_width = self._padding_left + self._padding_right + 2 * self._border_width base_height = self._padding_top + self._padding_bottom + 2 * self._border_width @@ -121,7 +136,7 @@ def fit_content(self): ) @property - def text(self): + def text(self) return self.label.text @text.setter @@ -148,17 +163,22 @@ def do_render(self, surface: Surface): class UITextWidget(UIAnchorLayout): """ - Adds the ability to add text to a widget. - The text can be placed within the widget using UIAnchorLayout parameters with `place_text()`. + Adds the ability to add text to a widget. + + The text can be placed within the widget using + :py:class:`~arcade.gui.UIAnchorLayout` parameters with + :py:meth:`~arcade.gui.UITextWidget.place_text`. """ def __init__(self, text: str = "", multiline: bool = False, **kwargs): super().__init__(text=text, **kwargs) + self._label = UILabel( text=text, multiline=multiline, width=1000 if multiline else None ) # width 1000 try to prevent line wrap if multiline is enabled + self.add(self._label) self.ui_label.fit_content() @@ -171,7 +191,8 @@ def place_text(self, align_y: float = 0, **kwargs): """ - This allows to place widgets text within the widget using UIAnchorLayout parameters. + Place widget's text within the widget using + :py:class:`~arcade.gui.UIAnchorLayout` parameters. """ self.remove(self._label) self.add( @@ -185,6 +206,9 @@ def place_text(self, @property def text(self): + """Text of the widget. Modifying this repeatedly will cause significant + lag; calculating glyph position is very expensive. + """ return self._label.text @text.setter @@ -195,6 +219,10 @@ def text(self, value): @property def multiline(self): + """Multiline state of the widget. Newlines indicated with ``\n`` will + only be honored if this is set to true. If you want a scrollable text + widget, use :py:class:`~arcade.gui.TextArea`. + """ return self.label.multiline @multiline.setter @@ -205,6 +233,9 @@ def multiline(self, value): @property def ui_label(self) -> UILabel: + """Internal :py:class:`~arcade.gui.UILabel` used for rendering the + text. + """ return self._label @property @@ -214,24 +245,39 @@ def label(self) -> arcade.Text: class UIInputText(UIWidget): """ - An input field the user can type text into. - - :param float x: x coordinate of bottom left - :param float y: y coordinate of bottom left - :param width: width of widget - :param height: height of widget - :param text: Text to show - :param font_name: string or tuple of font names, to load - :param font_size: size of the text - :param text_color: color of the text - :param multiline: support for multiline - :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 + 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. + + A mouse drag selects text, a mouse press moves the caret, and keys can move + 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 float x: x position (default anchor is bottom-left). + :param float 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: RGB color of the caret. + :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. """ - # move layout one pixel into the scissor box, so the caret is also shown at position 0 + # Move layout one pixel into the scissor box so the caret is also shown at + # position 0. LAYOUT_OFFSET = 1 def __init__( @@ -245,6 +291,7 @@ def __init__( font_size: float = 12, text_color: RGBA255 = (0, 0, 0, 255), multiline=False, + caret_color: RGB = (0, 0, 0), size_hint=None, size_hint_min=None, size_hint_max=None, @@ -275,13 +322,15 @@ def __init__( self.doc, width - self.LAYOUT_OFFSET, height, multiline=multiline ) self.layout.x += self.LAYOUT_OFFSET - self.caret = Caret(self.layout, color=(0, 0, 0)) + self.caret = Caret(self.layout, color=caret_color) self.caret.visible = False self._blink_state = self._get_caret_blink_state() def _get_caret_blink_state(self): - return self.caret.visible and self._active and self.caret._blink_visible + """Check whether or not the caret is currently blinking or not.""" + return self.caret.visible and self._active and \ + self.caret._blink_visible def on_update(self, dt): # Only trigger render if blinking state changed @@ -291,7 +340,7 @@ def on_update(self, dt): self.trigger_full_render() def on_event(self, event: UIEvent) -> Optional[bool]: - # if not active, check to activate, return + # If not active, check to activate, return if not self._active and isinstance(event, UIMousePressEvent): if self.rect.collide_with_point(event.x, event.y): self._active = True @@ -300,7 +349,7 @@ def on_event(self, event: UIEvent) -> Optional[bool]: self.caret.position = len(self.doc.text) return EVENT_UNHANDLED - # if active check to deactivate + # If active check to deactivate if self._active and isinstance(event, UIMousePressEvent): if self.rect.collide_with_point(event.x, event.y): x, y = event.x - self.x - self.LAYOUT_OFFSET, event.y - self.y @@ -311,7 +360,7 @@ def on_event(self, event: UIEvent) -> Optional[bool]: self.caret.on_deactivate() return EVENT_UNHANDLED - # if active pass all non press events to caret + # If active pass all non press events to caret if self._active: # Act on events if active if isinstance(event, UITextEvent): @@ -324,17 +373,20 @@ def on_event(self, event: UIEvent) -> Optional[bool]: self.caret.on_text_motion_select(event.selection) self.trigger_full_render() - if isinstance(event, UIMouseEvent) and self.rect.collide_with_point( + if isinstance(event, UIMouseEvent) and \ + self.rect.collide_with_point( event.x, event.y ): x, y = event.x - self.x - self.LAYOUT_OFFSET, event.y - self.y if isinstance(event, UIMouseDragEvent): self.caret.on_mouse_drag( - x, y, event.dx, event.dy, event.buttons, event.modifiers + x, y, event.dx, event.dy, + event.buttons, event.modifiers ) self.trigger_full_render() elif isinstance(event, UIMouseScrollEvent): - self.caret.on_mouse_scroll(x, y, event.scroll_x, event.scroll_y) + self.caret.on_mouse_scroll( + x, y, event.scroll_x, event.scroll_y) self.trigger_full_render() if super().on_event(event): @@ -371,23 +423,27 @@ def do_render(self, surface: Surface): class UITextArea(UIWidget): """ - A text area for scrollable text. - - - :param float x: x coordinate of bottom left - :param float y: y coordinate of bottom left - :param width: width of widget - :param height: height of widget - :param text: Text to show - :param font_name: string or tuple of font names, to load - :param font_size: size of the text - :param text_color: color of the text - :param multiline: support for multiline - :param scroll_speed: speed of scrolling - :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 + A text area that allows users to view large documents of text by scrolling + the mouse. + + + :param float x: x position (default anchor is bottom-left). + :param float 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. """ def __init__( @@ -420,7 +476,8 @@ def __init__( # Set how fast the mouse scroll wheel will scroll text in the pane. # Measured in pixels per 'click' - self.scroll_speed = scroll_speed if scroll_speed is not None else font_size + self.scroll_speed = scroll_speed if scroll_speed is not None \ + else font_size self.doc: AbstractDocument = pyglet.text.decode_text(text) self.doc.set_style( @@ -444,7 +501,7 @@ def __init__( def fit_content(self): """ - Sets the width and height of this UIWidget to contain the whole text. + Set the width and height of the text area to contain the whole text. """ self.rect = Rect( self.x, From f3ed9b311620308d759994544dc098fc81d23ce0 Mon Sep 17 00:00:00 2001 From: Ethan Chan Date: Fri, 7 Apr 2023 08:28:58 -0700 Subject: [PATCH 5/9] Minor formatting fixes * Fixed extra trailing whitespace * Fixed missing colon * Fixed missing import --- arcade/gui/widgets/text.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/arcade/gui/widgets/text.py b/arcade/gui/widgets/text.py index 2fcdb3b7ba..9628be5de0 100644 --- a/arcade/gui/widgets/text.py +++ b/arcade/gui/widgets/text.py @@ -19,7 +19,7 @@ from arcade.gui.property import bind from arcade.gui.widgets import UIWidget, Surface, Rect from arcade.gui.widgets.layout import UIAnchorLayout -from arcade.types import RGBA255, Color +from arcade.types import RGB, RGBA255, Color class UILabel(UIWidget): @@ -136,7 +136,7 @@ def fit_content(self): ) @property - def text(self) + def text(self): return self.label.text @text.setter @@ -163,7 +163,7 @@ 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. The text can be placed within the widget using :py:class:`~arcade.gui.UIAnchorLayout` parameters with From 5f5f00f57d6be4cd472b584f5f0e000b905d3d69 Mon Sep 17 00:00:00 2001 From: Ethan Chan Date: Fri, 7 Apr 2023 19:09:40 -0700 Subject: [PATCH 6/9] Fixes for `arcade.text` Thanks @gran4! --- arcade/gui/widgets/text.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/arcade/gui/widgets/text.py b/arcade/gui/widgets/text.py index 9628be5de0..4206812b3e 100644 --- a/arcade/gui/widgets/text.py +++ b/arcade/gui/widgets/text.py @@ -57,7 +57,6 @@ class UILabel(UIWidget): :py:class:`~arcade.gui.UITextWidget` with ``multiline`` of True is the same thing as a :py:class:`~arcade.gui.UITextArea`. - :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. @@ -206,7 +205,8 @@ def place_text(self, @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._label.text @@ -219,9 +219,10 @@ def text(self, value): @property def multiline(self): - """Multiline state of the widget. Newlines indicated with ``\n`` will - only be honored if this is set to true. If you want a scrollable text - widget, use :py:class:`~arcade.gui.TextArea`. + """ + Multiline state of the widget. Newlines indicated with ``\n`` will only + be honored if this is set to true. If you want a scrollable text widget, + use :py:class:`~arcade.gui.TextArea`. """ return self.label.multiline @@ -233,8 +234,8 @@ def multiline(self, value): @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 From 14ffe0acf6981935ee1d86c0464b81de76364130 Mon Sep 17 00:00:00 2001 From: Ethan Chan <103769713+eschan145@users.noreply.github.com> Date: Wed, 19 Apr 2023 09:19:44 -0700 Subject: [PATCH 7/9] Small commit checks and fixes --- arcade/gui/widgets/text.py | 1 - 1 file changed, 1 deletion(-) diff --git a/arcade/gui/widgets/text.py b/arcade/gui/widgets/text.py index 4206812b3e..31c5192476 100644 --- a/arcade/gui/widgets/text.py +++ b/arcade/gui/widgets/text.py @@ -427,7 +427,6 @@ class UITextArea(UIWidget): A text area that allows users to view large documents of text by scrolling the mouse. - :param float x: x position (default anchor is bottom-left). :param float y: y position (default anchor is bottom-left). :param width: Width of the text area. From 32d27199dbe370934b3051c98f53f9c55fdd68b8 Mon Sep 17 00:00:00 2001 From: Maic Siemering Date: Tue, 9 May 2023 20:44:07 +0200 Subject: [PATCH 8/9] Update arcade/gui/widgets/text.py Co-authored-by: Paul <36696816+pushfoo@users.noreply.github.com> --- arcade/gui/widgets/text.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/arcade/gui/widgets/text.py b/arcade/gui/widgets/text.py index 31c5192476..1232675863 100644 --- a/arcade/gui/widgets/text.py +++ b/arcade/gui/widgets/text.py @@ -220,9 +220,11 @@ def text(self, value): @property def multiline(self): """ - Multiline state of the widget. Newlines indicated with ``\n`` will only - be honored if this is set to true. If you want a scrollable text widget, - use :py:class:`~arcade.gui.TextArea`. + 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` + instead. """ return self.label.multiline From 8b4b705d04f9e992d431ba053ceca2a8173a83cb Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Fri, 9 Jun 2023 03:03:42 +0200 Subject: [PATCH 9/9] Update text.py --- arcade/gui/widgets/text.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arcade/gui/widgets/text.py b/arcade/gui/widgets/text.py index 0e3a9a1828..b1e8e52308 100644 --- a/arcade/gui/widgets/text.py +++ b/arcade/gui/widgets/text.py @@ -20,7 +20,7 @@ from arcade.gui.surface import Surface from arcade.gui.widgets import UIWidget, Rect from arcade.gui.widgets.layout import UIAnchorLayout -from arcade.types import RGBA255, Color, RGBOrA255 +from arcade.types import RGBA255, Color, RGBOrA255, RGB