Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ Arcade [PyPi Release History](https://pypi.org/project/arcade/#history) page.

## Unreleased

### New Features
- GUI: `UIDropdown` now supports scrolling when options exceed the menu height. New parameters: `max_height`, `invert_scroll`, `scroll_speed`, and `show_scroll_bar`.

### Breaking Change
- Tilemap: Sprites of an object tile layer will now apply visibility of the object.
- Tilemap: Sprites of an object tile layer will now apply visibility of the object.

## 4.0.0.dev3

Expand Down
3 changes: 2 additions & 1 deletion arcade/examples/gui/2_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,8 @@ def _show_interactive_widgets(self):
dropdown_row.add(
UIDropdown(
default="Option 1",
options=["Option 1", "Option 2", "Option 3"],
options=[f"Option {i}" for i in range(1, 16)],
show_scroll_bar=True,
)
)

Expand Down
93 changes: 80 additions & 13 deletions arcade/gui/widgets/dropdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,63 @@
from arcade.gui.events import UIControllerButtonPressEvent, UIOnChangeEvent, UIOnClickEvent
from arcade.gui.experimental import UIScrollArea
from arcade.gui.experimental.focus import UIFocusMixin
from arcade.gui.experimental.scroll_area import UIScrollBar
from arcade.gui.ui_manager import UIManager
from arcade.gui.widgets import UILayout, UIWidget
from arcade.gui.widgets.buttons import UIFlatButton
from arcade.gui.widgets.layout import UIBoxLayout


class _UIDropdownOverlay(UIFocusMixin, UIBoxLayout):
"""Represents the dropdown options overlay.
"""Represents the dropdown options overlay with scroll support.

Currently only handles closing the overlay when clicked outside of the options.
Contains a UIScrollArea with the option buttons and a UIScrollBar
for navigating when options exceed the maximum height.
"""

# TODO move also options logic to this class
SCROLL_BAR_WIDTH = 15

def __init__(
self,
max_height: float = 200,
invert_scroll: bool = False,
scroll_speed: float = 15.0,
show_scroll_bar: bool = False,
):
# Horizontal layout: [scroll_area | scroll_bar]
# size_hint=None prevents UIManager from overriding the rect
# that UIDropdown.do_layout explicitly sets.
super().__init__(vertical=False, align="top", size_hint=None)
self._max_height = max_height
self._show_scroll_bar = show_scroll_bar

self._options_layout = UIBoxLayout(size_hint=(1, 0))
self._scroll_area = UIScrollArea(
width=100,
height=100,
canvas_size=(100, 100),
size_hint=(1, 1),
)
self._scroll_area.invert_scroll = invert_scroll
self._scroll_area.scroll_speed = scroll_speed
self._scroll_area.add(self._options_layout)

super().add(self._scroll_area)

if show_scroll_bar:
self._scroll_bar = UIScrollBar(self._scroll_area, vertical=True)
self._scroll_bar.size_hint = (None, 1)
self._scroll_bar.rect = self._scroll_bar.rect.resize(width=self.SCROLL_BAR_WIDTH)
super().add(self._scroll_bar)

def add_option(self, widget: UIWidget) -> UIWidget:
"""Add an option widget to the options layout."""
return self._options_layout.add(widget)

def clear_options(self):
"""Clear all options and reset scroll position."""
self._options_layout.clear()
self._scroll_area.scroll_y = 0

def show(self, manager: UIManager | UIScrollArea):
manager.add(self, layer=UIManager.OVERLAY_LAYER)
Expand Down Expand Up @@ -67,6 +111,10 @@ def on_change(event: UIOnChangeEvent):
height: Height of each of the option.
default: The default value shown.
options: The options displayed when the layout is clicked.
max_height: Maximum height of the dropdown menu before scrolling is enabled.
invert_scroll: Invert the scroll direction of the dropdown menu.
scroll_speed: Speed of scrolling in the dropdown menu.
show_scroll_bar: Show a scroll bar in the dropdown menu.
primary_style: The style of the primary button.
dropdown_style: The style of the buttons in the dropdown.
active_style: The style of the dropdown button, which represents the active option.
Expand Down Expand Up @@ -120,6 +168,10 @@ def __init__(
height: float = 30,
default: str | None = None,
options: list[str | None] | None = None,
max_height: float = 200,
invert_scroll: bool = False,
scroll_speed: float = 15.0,
show_scroll_bar: bool = False,
primary_style=None,
dropdown_style=None,
active_style=None,
Expand Down Expand Up @@ -150,7 +202,12 @@ def __init__(
)
self._default_button.on_click = self._on_button_click # type: ignore

self._overlay = _UIDropdownOverlay()
self._overlay = _UIDropdownOverlay(
max_height=max_height,
invert_scroll=invert_scroll,
scroll_speed=scroll_speed,
show_scroll_bar=show_scroll_bar,
)
self._update_options()

# add children after super class setup
Expand All @@ -176,16 +233,16 @@ def value(self, value: str | None):

def _update_options(self):
# generate options
self._overlay.clear()
self._overlay.clear_options()

for option in self._options:
if option is None: # None = UIDropdown.DIVIDER, required by pyright
self._overlay.add(
self._overlay.add_option(
UIWidget(width=self.width, height=2).with_background(color=arcade.color.GRAY)
)
continue
else:
button = self._overlay.add(
button = self._overlay.add_option(
UIFlatButton(
text=option,
width=self.width,
Expand Down Expand Up @@ -225,13 +282,23 @@ def do_layout(self):
but is required for the dropdown."""
self._default_button.rect = self.rect

# resize layout to contain widgets
overlay = self._overlay
rect = overlay.rect
if overlay.size_hint_min is not None:
rect = rect.resize(*overlay.size_hint_min)
# Calculate total options height
total_h = 0
for option in self._options:
total_h += 2 if option is None else self.height

self._overlay.rect = rect.align_top(self.bottom - 2).align_left(self._default_button.left)
# Cap at max_height
overlay = self._overlay
visible_h = min(total_h, overlay._max_height) if total_h > 0 else self.height
scroll_bar_w = _UIDropdownOverlay.SCROLL_BAR_WIDTH if overlay._show_scroll_bar else 0
overlay_w = self.width + scroll_bar_w

overlay.rect = (
overlay.rect
.resize(overlay_w, visible_h)
.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
Expand Down
5 changes: 5 additions & 0 deletions doc/tutorials/menu/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,11 @@ Adding it to the widget layout.
:caption: Adding dropdown to the layout
:lines: 242

If a dropdown has many options, it will automatically scroll when the list
exceeds the ``max_height`` (default 200px). You can also enable a visible
scroll bar with ``show_scroll_bar=True``, control the scroll direction with
``invert_scroll``, and adjust the scroll speed with ``scroll_speed``.

Adding a Slider
~~~~~~~~~~~~~~~

Expand Down
37 changes: 34 additions & 3 deletions doc/tutorials/menu/menu_05.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,20 @@ def on_click_volume_button(event):
"Volume Menu",
"How do you like your volume?",
"Enable Sound",
["Play: Rock", "Play: Punk", "Play: Pop"],
[
"Play: Rock",
"Play: Punk",
"Play: Pop",
"Play: Jazz",
"Play: Blues",
"Play: Classical",
"Play: Country",
"Play: Electronic",
"Play: Hip Hop",
"Play: Metal",
"Play: R&B",
"Play: Reggae",
],
"Adjust Volume",
)
self.manager.add(volume_menu, layer=1)
Expand All @@ -130,7 +143,20 @@ def on_click_options_button(event):
"Funny Menu",
"Too much fun here",
"Fun?",
["Make Fun", "Enjoy Fun", "Like Fun"],
[
"Make Fun",
"Enjoy Fun",
"Like Fun",
"Share Fun",
"Spread Fun",
"Find Fun",
"Create Fun",
"Discover Fun",
"Embrace Fun",
"Celebrate Fun",
"Inspire Fun",
"Maximize Fun",
],
"Adjust Fun",
)
self.manager.add(options_menu, layer=1)
Expand Down Expand Up @@ -216,8 +242,13 @@ def __init__(
toggle_group.add(toggle_label)

# Create dropdown with a specified default.
# When many options are provided, the dropdown automatically scrolls.
dropdown = arcade.gui.UIDropdown(
default=dropdown_options[0], options=dropdown_options, height=20, width=250
default=dropdown_options[0],
options=dropdown_options,
height=20,
width=250,
show_scroll_bar=True,
)

slider_label = arcade.gui.UILabel(text=slider_label)
Expand Down
Loading
Loading