88from arcade .gui .events import UIControllerButtonPressEvent , UIOnChangeEvent , UIOnClickEvent
99from arcade .gui .experimental import UIScrollArea
1010from arcade .gui .experimental .focus import UIFocusMixin
11+ from arcade .gui .experimental .scroll_area import UIScrollBar
1112from arcade .gui .ui_manager import UIManager
1213from arcade .gui .widgets import UILayout , UIWidget
1314from arcade .gui .widgets .buttons import UIFlatButton
1415from arcade .gui .widgets .layout import UIBoxLayout
1516
1617
1718class _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