Skip to content

Commit 0117d29

Browse files
Add scrollbar support to UIDropdown
The dropdown menu now wraps options in a UIScrollArea with a UIScrollBar, preventing the menu from extending beyond the window when there are many options. A new max_height parameter (default 200px) controls when scrolling activates. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent bbf221e commit 0117d29

1 file changed

Lines changed: 56 additions & 13 deletions

File tree

arcade/gui/widgets/dropdown.py

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,51 @@
88
from arcade.gui.events import UIControllerButtonPressEvent, UIOnChangeEvent, UIOnClickEvent
99
from arcade.gui.experimental import UIScrollArea
1010
from arcade.gui.experimental.focus import UIFocusMixin
11+
from arcade.gui.experimental.scroll_area import UIScrollBar
1112
from arcade.gui.ui_manager import UIManager
1213
from arcade.gui.widgets import UILayout, UIWidget
1314
from arcade.gui.widgets.buttons import UIFlatButton
1415
from arcade.gui.widgets.layout import UIBoxLayout
1516

1617

1718
class _UIDropdownOverlay(UIFocusMixin, UIBoxLayout):
18-
"""Represents the dropdown options overlay.
19+
"""Represents the dropdown options overlay with scroll support.
1920
20-
Currently only handles closing the overlay when clicked outside of the options.
21+
Contains a UIScrollArea with the option buttons and a UIScrollBar
22+
for navigating when options exceed the maximum height.
2123
"""
2224

23-
# TODO move also options logic to this class
25+
SCROLL_BAR_WIDTH = 15
26+
27+
def __init__(self, max_height: float = 200):
28+
# Horizontal layout: [scroll_area | scroll_bar]
29+
super().__init__(vertical=False, align="top", size_hint=(0, 0))
30+
self._max_height = max_height
31+
32+
self._options_layout = UIBoxLayout(size_hint=(1, 0))
33+
self._scroll_area = UIScrollArea(
34+
width=100,
35+
height=100,
36+
canvas_size=(100, 100),
37+
size_hint=(1, 1),
38+
)
39+
self._scroll_area.add(self._options_layout)
40+
41+
self._scroll_bar = UIScrollBar(self._scroll_area, vertical=True)
42+
self._scroll_bar.size_hint = (None, 1)
43+
self._scroll_bar.rect = self._scroll_bar.rect.resize(width=self.SCROLL_BAR_WIDTH)
44+
45+
super().add(self._scroll_area)
46+
super().add(self._scroll_bar)
47+
48+
def add_option(self, widget: UIWidget) -> UIWidget:
49+
"""Add an option widget to the options layout."""
50+
return self._options_layout.add(widget)
51+
52+
def clear_options(self):
53+
"""Clear all options and reset scroll position."""
54+
self._options_layout.clear()
55+
self._scroll_area.scroll_y = 0
2456

2557
def show(self, manager: UIManager | UIScrollArea):
2658
manager.add(self, layer=UIManager.OVERLAY_LAYER)
@@ -67,6 +99,7 @@ def on_change(event: UIOnChangeEvent):
6799
height: Height of each of the option.
68100
default: The default value shown.
69101
options: The options displayed when the layout is clicked.
102+
max_height: Maximum height of the dropdown menu before scrolling is enabled.
70103
primary_style: The style of the primary button.
71104
dropdown_style: The style of the buttons in the dropdown.
72105
active_style: The style of the dropdown button, which represents the active option.
@@ -120,6 +153,7 @@ def __init__(
120153
height: float = 30,
121154
default: str | None = None,
122155
options: list[str | None] | None = None,
156+
max_height: float = 200,
123157
primary_style=None,
124158
dropdown_style=None,
125159
active_style=None,
@@ -150,7 +184,7 @@ def __init__(
150184
)
151185
self._default_button.on_click = self._on_button_click # type: ignore
152186

153-
self._overlay = _UIDropdownOverlay()
187+
self._overlay = _UIDropdownOverlay(max_height=max_height)
154188
self._update_options()
155189

156190
# add children after super class setup
@@ -176,16 +210,16 @@ def value(self, value: str | None):
176210

177211
def _update_options(self):
178212
# generate options
179-
self._overlay.clear()
213+
self._overlay.clear_options()
180214

181215
for option in self._options:
182216
if option is None: # None = UIDropdown.DIVIDER, required by pyright
183-
self._overlay.add(
217+
self._overlay.add_option(
184218
UIWidget(width=self.width, height=2).with_background(color=arcade.color.GRAY)
185219
)
186220
continue
187221
else:
188-
button = self._overlay.add(
222+
button = self._overlay.add_option(
189223
UIFlatButton(
190224
text=option,
191225
width=self.width,
@@ -225,13 +259,22 @@ def do_layout(self):
225259
but is required for the dropdown."""
226260
self._default_button.rect = self.rect
227261

228-
# resize layout to contain widgets
229-
overlay = self._overlay
230-
rect = overlay.rect
231-
if overlay.size_hint_min is not None:
232-
rect = rect.resize(*overlay.size_hint_min)
262+
# Calculate total options height
263+
total_h = 0
264+
for option in self._options:
265+
total_h += 2 if option is None else self.height
233266

234-
self._overlay.rect = rect.align_top(self.bottom - 2).align_left(self._default_button.left)
267+
# Cap at max_height
268+
overlay = self._overlay
269+
visible_h = min(total_h, overlay._max_height) if total_h > 0 else self.height
270+
overlay_w = self.width + _UIDropdownOverlay.SCROLL_BAR_WIDTH
271+
272+
overlay.rect = (
273+
overlay.rect
274+
.resize(overlay_w, visible_h)
275+
.align_top(self.bottom - 2)
276+
.align_left(self._default_button.left)
277+
)
235278

236279
def on_change(self, event: UIOnChangeEvent):
237280
"""To be implemented by the user, triggered when the current selected value

0 commit comments

Comments
 (0)