Repository: MyreMylar/pygame_gui (python)

Issue:
Item inside UIDropDownMenu should auto scroll to and selected when in expanded state
**Is your feature request related to a problem? Please describe.**
If UIDropDownMenu has many items, whenever I click the expand button, it just shows item start from index 0 of options_list whatever the item I have chosen.


**Describe the solution you'd like**
the UIExpandedDropDownState should find the right item of selected_option, then focus/select it, if it is not in visible area, auto scroll to right pos.

**Describe alternatives you've considered**
None

**Additional context**
can be observed on pygame_gui_examples/general_ui_test_app.py , test_drop_down_menu, expand it, choice item 28, close it , expand again, just show item1~item7


Pull request description:
Change drop down to scroll to selected item in list
This happens when the drop down options list is expanded. The currently selected option is now highlighted in the list and, if necessary, the list is scrolled to include the option as near to the top of the visible options as possible.

fixes #161 

Patch:
diff --git a/pygame_gui/elements/ui_drop_down_menu.py b/pygame_gui/elements/ui_drop_down_menu.py
index 2e1b663..12ce795 100644
--- a/pygame_gui/elements/ui_drop_down_menu.py
+++ b/pygame_gui/elements/ui_drop_down_menu.py
@@ -204,8 +204,15 @@ class UIExpandedDropDownState:
                                                       parent_element=self.drop_down_menu_ui,
                                                       container=self.drop_down_menu_ui.ui_container,
                                                       anchors=self.drop_down_menu_ui.anchors,
-                                                      object_id='#drop_down_options_list')
+                                                      object_id='#drop_down_options_list',
+                                                      default_selection=self.selected_option)
         self.drop_down_menu_ui.join_focus_sets(self.options_selection_list)
+        if self.options_selection_list.scroll_bar is not None:
+            # our options list is long enough to have a scroll bar.
+            # we want to scroll it enough that the currently selected option is on screen.
+            start_percentage = self.options_selection_list.get_single_selection_start_percentage()
+            self.options_selection_list.scroll_bar.set_scroll_from_start_percentage(start_percentage)
+            self.options_selection_list.update(0.0)
 
         if should_rebuild:
             self.rebuild()
diff --git a/pygame_gui/elements/ui_horizontal_scroll_bar.py b/pygame_gui/elements/ui_horizontal_scroll_bar.py
index 857a5c6..57a1675 100644
--- a/pygame_gui/elements/ui_horizontal_scroll_bar.py
+++ b/pygame_gui/elements/ui_horizontal_scroll_bar.py
@@ -337,6 +337,26 @@ class UIHorizontalScrollBar(UIElement):
                 if not self.has_moved_recently:
                     self.has_moved_recently = True
 
+    def set_scroll_from_start_percentage(self, new_start_percentage: float):
+        """
+        Set the scroll bar's scrolling position from a percentage between 0.0 and 1.0.
+
+        :param new_start_percentage: the percentage to set.
+        """
+        new_start_percentage = min(1.0, max(new_start_percentage, 0.0))
+        self.start_percentage = new_start_percentage
+
+        new_scroll_position = new_start_percentage * self.scrollable_width
+
+        self.scroll_position = min(max(new_scroll_position, self.left_limit),
+                                   self.right_limit - self.sliding_button.rect.width)
+        self.start_percentage = self.scroll_position / self.scrollable_width
+
+        x_pos = (self.scroll_position + self.arrow_button_width)
+        y_pos = 0
+        self.sliding_button.set_relative_position((x_pos, y_pos))
+        self.has_moved_recently = True
+
     def redraw_scrollbar(self):
         """
         Redraws the 'scrollbar' portion of the whole UI element. Called when we change the
diff --git a/pygame_gui/elements/ui_selection_list.py b/pygame_gui/elements/ui_selection_list.py
index 5e48d24..923ad60 100644
--- a/pygame_gui/elements/ui_selection_list.py
+++ b/pygame_gui/elements/ui_selection_list.py
@@ -210,6 +210,15 @@ class UISelectionList(UIElement):
                         item['button_element'].kill()
                         item['button_element'] = None
 
+    def get_single_selection_start_percentage(self):
+        """
+        The percentage through the height of the list where the top of the first selected option is.
+        """
+        selected_item_heights = [item['height'] for item in self.item_list if item['selected']]
+        if len(selected_item_heights) > 0:
+            return float(selected_item_heights[0] / self.total_height_of_list)
+        return 0.0
+
     def set_item_list(self, new_item_list: Union[List[str], List[Tuple[str, str]]]):
         """
         Set a new string list (or tuple of strings & ids list) as the item list for this selection
@@ -227,21 +236,25 @@ class UISelectionList(UIElement):
         """
         self._raw_item_list = new_item_list
         self.item_list = []  # type: List[Dict]
+        index = 0
         for new_item in new_item_list:
             if isinstance(new_item, str):
                 new_item_list_item = {'text': new_item,
                                       'button_element': None,
                                       'selected': False,
-                                      'object_id': '#item_list_item'}
+                                      'object_id': '#item_list_item',
+                                      'height': index * self.list_item_height}
             elif isinstance(new_item, tuple):
                 new_item_list_item = {'text': new_item[0],
                                       'button_element': None,
                                       'selected': False,
-                                      'object_id': new_item[1]}
+                                      'object_id': new_item[1],
+                                      'height': index * self.list_item_height}
             else:
                 raise ValueError('Invalid item list')
 
             self.item_list.append(new_item_list_item)
+            index += 1
 
         self.total_height_of_list = self.list_item_height * len(self.item_list)
         self.lowest_list_pos = (self.total_height_of_list -
diff --git a/pygame_gui/elements/ui_vertical_scroll_bar.py b/pygame_gui/elements/ui_vertical_scroll_bar.py
index 94417db..4932761 100644
--- a/pygame_gui/elements/ui_vertical_scroll_bar.py
+++ b/pygame_gui/elements/ui_vertical_scroll_bar.py
@@ -334,6 +334,26 @@ class UIVerticalScrollBar(UIElement):
                 if not self.has_moved_recently:
                     self.has_moved_recently = True
 
+    def set_scroll_from_start_percentage(self, new_start_percentage: float):
+        """
+        Set the scroll bar's scrolling position from a percentage between 0.0 and 1.0.
+
+        :param new_start_percentage: the percentage to set.
+        """
+        new_start_percentage = min(1.0, max(new_start_percentage, 0.0))
+        self.start_percentage = new_start_percentage
+
+        new_scroll_position = new_start_percentage * self.scrollable_height
+
+        self.scroll_position = min(max(new_scroll_position, self.top_limit),
+                                   self.bottom_limit - self.sliding_button.rect.height)
+        self.start_percentage = self.scroll_position / self.scrollable_height
+
+        x_pos = 0
+        y_pos = (self.scroll_position + self.arrow_button_height)
+        self.sliding_button.set_relative_position((x_pos, y_pos))
+        self.has_moved_recently = True
+
     def redraw_scrollbar(self):
         """
         Redraws the 'scrollbar' portion of the whole UI element. Called when we change the


Test patch:
diff --git a/tests/test_elements/test_ui_horizontal_scroll_bar.py b/tests/test_elements/test_ui_horizontal_scroll_bar.py
index 14f0910..1a14f44 100644
--- a/tests/test_elements/test_ui_horizontal_scroll_bar.py
+++ b/tests/test_elements/test_ui_horizontal_scroll_bar.py
@@ -366,6 +366,17 @@ class TestUIHorizontalScrollBar:
         manager.draw_ui(surface)
         assert compare_surfaces(empty_surface, surface)
 
+    def test_set_scroll_from_start_percentage(self, _init_pygame, default_ui_manager, _display_surface_return_none):
+        scroll_bar = UIHorizontalScrollBar(relative_rect=pygame.Rect(0, 0, 146, 30),
+                                           visible_percentage=0.2,
+                                           manager=default_ui_manager)
+        scroll_bar.reset_scroll_position()
+        assert scroll_bar.scroll_position == 0.0 and scroll_bar.start_percentage == 0.0
+
+        scroll_bar.set_scroll_from_start_percentage(0.5)
+        assert scroll_bar.scrollable_width == 100
+        assert scroll_bar.scroll_position == 50.0 and scroll_bar.start_percentage == 0.5
+
 
 if __name__ == '__main__':
     pytest.console_main()
diff --git a/tests/test_elements/test_ui_selection_list.py b/tests/test_elements/test_ui_selection_list.py
index a2acdd6..9116195 100644
--- a/tests/test_elements/test_ui_selection_list.py
+++ b/tests/test_elements/test_ui_selection_list.py
@@ -1015,6 +1015,26 @@ class TestUISelectionList:
 
         assert selection_list.get_multi_selection() == ['green']
 
+    def test_get_single_selection_start_percentage(self, _init_pygame, default_ui_manager,
+                                                   _display_surface_return_none):
+        selection_list = UISelectionList(relative_rect=pygame.Rect(50, 50, 150, 400),
+                                         item_list=['green', 'eggs', 'and', 'ham'],
+                                         manager=default_ui_manager,
+                                         default_selection='green')
+        assert selection_list.get_single_selection_start_percentage() == 0.0
+
+        selection_list = UISelectionList(relative_rect=pygame.Rect(50, 50, 150, 400),
+                                         item_list=['green', 'eggs', 'and', 'ham'],
+                                         manager=default_ui_manager,
+                                         default_selection='and')
+        assert selection_list.get_single_selection_start_percentage() == 0.5
+
+        selection_list = UISelectionList(relative_rect=pygame.Rect(50, 50, 150, 400),
+                                         item_list=['green', 'eggs', 'and', 'ham'],
+                                         manager=default_ui_manager,
+                                         allow_multi_select=True)
+        assert selection_list.get_single_selection_start_percentage() == 0.0
+
 
 if __name__ == '__main__':
     pytest.console_main()
diff --git a/tests/test_elements/test_ui_vertical_scroll_bar.py b/tests/test_elements/test_ui_vertical_scroll_bar.py
index 1828162..346287e 100644
--- a/tests/test_elements/test_ui_vertical_scroll_bar.py
+++ b/tests/test_elements/test_ui_vertical_scroll_bar.py
@@ -354,6 +354,17 @@ class TestUIVerticalScrollBar:
         manager.draw_ui(surface)
         assert compare_surfaces(empty_surface, surface)
 
+    def test_set_scroll_from_start_percentage(self, _init_pygame, default_ui_manager, _display_surface_return_none):
+        scroll_bar = UIVerticalScrollBar(relative_rect=pygame.Rect(0, 0, 30, 146),
+                                         visible_percentage=0.2,
+                                         manager=default_ui_manager)
+        scroll_bar.reset_scroll_position()
+        assert scroll_bar.scroll_position == 0.0 and scroll_bar.start_percentage == 0.0
+
+        scroll_bar.set_scroll_from_start_percentage(0.5)
+        assert scroll_bar.scrollable_height == 100
+        assert scroll_bar.scroll_position == 50.0 and scroll_bar.start_percentage == 0.5
+
 
 if __name__ == '__main__':
     pytest.console_main()