Repository: MyreMylar/pygame_gui
Pull request #585 (2024-05-02 18:12:26)

add options to disable copy & paste


Patch:
diff --git a/docs/source/quick_start.rst b/docs/source/quick_start.rst
index e3d07b1..104a15d 100644
--- a/docs/source/quick_start.rst
+++ b/docs/source/quick_start.rst
@@ -233,7 +233,7 @@ we can just compare the event's ui_element attribute with our hello_button varia
 Try running the code again and clicking on the button. If it's all worked you should see 'Hello World!' printed to the
 python console each time you click the button.
 
-Now that we've got the basics of the code up and running, let's try experimenting with a custom theme file in part 2 of
+Now that we've got the basics of the code up and running, let's try experimenting with a custom them file in part 2 of
 this quick start guide:
 
 Quick Start Guides
diff --git a/docs/source/theme_guide.rst b/docs/source/theme_guide.rst
index 478faae..e5e1494 100644
--- a/docs/source/theme_guide.rst
+++ b/docs/source/theme_guide.rst
@@ -134,11 +134,11 @@ within the colours block you can start to set individual colours by their IDs. I
     * - HSL
       - Degree, 2 Percentage Values
       - :example-hsl:`hsl(190deg, 40%, .5)`
-      - Hue, Saturation, Luminance
+      - Hue, Saturation, Luminence
     * - HSLA
       - Degree, 3 Percentage Values
       - :example-hsla:`hsla(10deg, 40%, .5, 0.7)`
-      - Hue, Saturation, Luminance, Alpha
+      - Hue, Saturation, Luminence, Alpha
     * - HSV
       - Degree, 2 Percentage Values
       - :example-hsv:`hsv(282deg, 63%, 92%)`
@@ -150,7 +150,7 @@ within the colours block you can start to set individual colours by their IDs. I
     * - CMY
       - 3 percentage values
       - :example-cmy:`cmy(.3, .5, .7)`
-      - Cyan, Magenta, Yellow
+      - Cyan, Magneta, Yellow
 
 .. list-table:: Value Types
     :widths: 10 40 20 20 20
diff --git a/docs/source/theme_reference/theme_progress_bar.rst b/docs/source/theme_reference/theme_progress_bar.rst
index b68dc74..416b894 100644
--- a/docs/source/theme_reference/theme_progress_bar.rst
+++ b/docs/source/theme_reference/theme_progress_bar.rst
@@ -23,7 +23,7 @@ Colours
 :class:`UIProgressBar <pygame_gui.elements.UIProgressBar>` makes use of these colour parameters in a 'colours' block. Most of these colours can
 also be a colour gradient:
 
- - "**normal_text**" - The colour/gradient of the health bar's text.
+ - "**normal_text**" - The colour/gradient of the health bars's text.
  - "**text_shadow**" - The colour of the shadow behind the text (so it stands out better).
  - "**normal_border**" - The colour/gradient of the border around the health bar.
  - "**filled_bar**" - The colour/gradient of the actual bar itself, of the portion of it that is still full.
diff --git a/docs/source/theme_reference/theme_screen_space_health_bar.rst b/docs/source/theme_reference/theme_screen_space_health_bar.rst
index b9c092e..e8c29c8 100644
--- a/docs/source/theme_reference/theme_screen_space_health_bar.rst
+++ b/docs/source/theme_reference/theme_screen_space_health_bar.rst
@@ -23,7 +23,7 @@ Colours
 :class:`UIScreenSpaceHealthBar <pygame_gui.elements.UIScreenSpaceHealthBar>` makes use of these colour parameters in a 'colours' block. Most of these colours can
 also be a colour gradient:
 
- - "**normal_text**" - The colour/gradient of the health bar's text.
+ - "**normal_text**" - The colour/gradient of the health bars's text.
  - "**text_shadow**" - The colour of the shadow behind the text (so it stands out better).
  - "**normal_border**" - The colour/gradient of the border around the health bar.
  - "**filled_bar**" - The colour/gradient of the actual bar itself, of the portion of it that is still full.
diff --git a/pygame_gui/core/utility.py b/pygame_gui/core/utility.py
index d0eec02..212f761 100644
--- a/pygame_gui/core/utility.py
+++ b/pygame_gui/core/utility.py
@@ -173,7 +173,7 @@ elif PLATFORM == 'LINUX':
             stdout, _ = process.communicate()
             return stdout.decode('utf-8')
 
-else:
+elif PLATFORM == 'DARWIN':
     def __mac_copy(data: str):
         with subprocess.Popen('pbcopy',
                               env={'LANG': 'en_US.UTF-8'},
@@ -185,15 +185,24 @@ else:
         return subprocess.check_output(
             'pbpaste', env={'LANG': 'en_US.UTF-8'}).decode('utf-8')
 
+else:
+    def __unknown_copy(data: str):
+        # copy not supported on this platform
+        pass
+
+    def __unknown_paste():
+        # paste not supported on this platform
+        return ""
+
 
 def clipboard_copy(data: str):
     """
-    Hopefully cross platform, copy to a clipboard.
+    Hopefully cross-platform, copy to a clipboard.
 
     :return: A platform specific copy function.
 
     """
-    if pygame.vernum.major == 2 and pygame.vernum.minor >= 2:
+    if (pygame.vernum.major == 2 and pygame.vernum.minor >= 2) or pygame.vernum.major > 2:
         pygame.scrap.put_text(data)
     else:
         current_platform = platform.system().upper()
@@ -201,18 +210,20 @@ def clipboard_copy(data: str):
             __windows_copy(data)
         elif current_platform == 'LINUX':
             __linux_copy(data)
-        else:
+        elif current_platform == 'DARWIN':
             __mac_copy(data)
+        else:
+            __unknown_copy(data)
 
 
 def clipboard_paste():
     """
-    Hopefully cross platform, paste from a clipboard.
+    Hopefully cross-platform, paste from a clipboard.
 
     :return: A platform specific paste function.
 
     """
-    if pygame.vernum.major == 2 and pygame.vernum.minor >= 2:
+    if (pygame.vernum.major == 2 and pygame.vernum.minor >= 2) or pygame.vernum.major > 2:
         return pygame.scrap.get_text()
     else:
         current_platform = platform.system().upper()
@@ -220,8 +231,10 @@ def clipboard_paste():
             return __windows_paste()
         elif current_platform == 'LINUX':
             return __linux_paste()
-        else:
+        elif current_platform == 'DARWIN':
             return __mac_paste()
+        else:
+            return __unknown_paste()
 
 
 def create_resource_path(relative_path: Union[str, Path]):
diff --git a/pygame_gui/elements/ui_scrolling_container.py b/pygame_gui/elements/ui_scrolling_container.py
index 5254647..e531619 100644
--- a/pygame_gui/elements/ui_scrolling_container.py
+++ b/pygame_gui/elements/ui_scrolling_container.py
@@ -87,7 +87,7 @@ class UIScrollingContainer(UIElement, IContainerLikeInterface):
         # scroll bars so that it resizes automatically as the scroll bars are added and "removed".
 
         # this contains the scroll bars and the 'view' container
-        self._root_container = UIContainer(relative_rect=self.relative_rect,
+        self._root_container = UIContainer(relative_rect=relative_rect,
                                            manager=manager,
                                            starting_height=starting_height,
                                            container=container,
@@ -106,7 +106,7 @@ class UIScrollingContainer(UIElement, IContainerLikeInterface):
         self.vert_scroll_bar = None
         if self.allow_scroll_y:
 
-            scroll_bar_rect = pygame.Rect(-20, 0, 20, self.relative_rect.height)
+            scroll_bar_rect = pygame.Rect(-20, 0, 20, relative_rect.height)
             self.vert_scroll_bar = UIVerticalScrollBar(relative_rect=scroll_bar_rect,
                                                        visible_percentage=1.0,
                                                        manager=self.ui_manager,
@@ -117,7 +117,7 @@ class UIScrollingContainer(UIElement, IContainerLikeInterface):
                                                                 'top': 'top',
                                                                 'bottom': 'bottom'})
 
-            self.vert_scroll_bar.set_dimensions((0, self.relative_rect.height))  # TODO: Remove these when anchoring is fixed
+            self.vert_scroll_bar.set_dimensions((0, relative_rect.height))  # TODO: Remove these when anchoring is fixed
             self.vert_scroll_bar.set_relative_position((0, 0))  # Without this, they look off-centered
             self.join_focus_sets(self.vert_scroll_bar)
 
@@ -125,7 +125,7 @@ class UIScrollingContainer(UIElement, IContainerLikeInterface):
 
         self.horiz_scroll_bar = None
         if self.allow_scroll_x:
-            scroll_bar_rect = pygame.Rect(0, -20, self.relative_rect.width, 20)
+            scroll_bar_rect = pygame.Rect(0, -20, relative_rect.width, 20)
             self.horiz_scroll_bar = UIHorizontalScrollBar(relative_rect=scroll_bar_rect,
                                                           visible_percentage=1.0,
                                                           manager=self.ui_manager,
@@ -136,7 +136,7 @@ class UIScrollingContainer(UIElement, IContainerLikeInterface):
                                                                    'top': 'bottom',
                                                                    'bottom': 'bottom'})
 
-            self.horiz_scroll_bar.set_dimensions((self.relative_rect.width, 0))
+            self.horiz_scroll_bar.set_dimensions((relative_rect.width, 0))
             self.horiz_scroll_bar.set_relative_position((0, 0))
             self.join_focus_sets(self.horiz_scroll_bar)
 
@@ -144,7 +144,7 @@ class UIScrollingContainer(UIElement, IContainerLikeInterface):
 
         # This container is the view on to the scrollable container it's size is determined by
         # the size of the root container and whether there are any scroll bars or not.
-        view_rect = pygame.Rect(0, 0, self.relative_rect.width, self.relative_rect.height)
+        view_rect = pygame.Rect(0, 0, relative_rect.width, relative_rect.height)
         self._view_container = UIContainer(relative_rect=view_rect,
                                            manager=manager,
                                            starting_height=1,
@@ -158,7 +158,7 @@ class UIScrollingContainer(UIElement, IContainerLikeInterface):
         # This container is what we actually put other stuff in.
         # It is aligned to the top left corner but that isn't that important for a container that
         # can be much larger than it's view
-        scrollable_rect = pygame.Rect(0, 0, self.relative_rect.width, self.relative_rect.height)
+        scrollable_rect = pygame.Rect(0, 0, relative_rect.width, relative_rect.height)
         scrollable_anchors = {'left': 'left',
                               'right': 'left',
                               'top': 'top',
@@ -415,7 +415,9 @@ class UIScrollingContainer(UIElement, IContainerLikeInterface):
             self.vert_scroll_bar.set_visible_percentage(vis_percent)
             self.vert_scroll_bar.set_relative_position((-self.scroll_bar_width, 0))
 
-            height = self._view_container.rect.height
+            # This is important as horiz scroll bar's dimensions are evaluated after. If horizontal scrollbar is not
+            # needed, then scroll bar height is 0.
+            height = self._view_container.rect.height - self.scroll_bar_height
             self.vert_scroll_bar.set_dimensions((self.scroll_bar_width,
                                                  height))
         else:
diff --git a/pygame_gui/elements/ui_text_box.py b/pygame_gui/elements/ui_text_box.py
index 38005fd..4a65360 100644
--- a/pygame_gui/elements/ui_text_box.py
+++ b/pygame_gui/elements/ui_text_box.py
@@ -199,6 +199,9 @@ class UITextBox(UIElement, IUITextOwnerInterface):
         self.vertical_cursor_movement = False
         self.last_horiz_cursor_index = 0
 
+        self.copy_text_enabled = True
+        self.paste_text_enabled = False
+
         self.rebuild_from_changed_theme_data()
 
     @property
@@ -922,7 +925,8 @@ class UITextBox(UIElement, IUITextOwnerInterface):
         if self.text_box_layout is not None and abs(self.select_range[0] - self.select_range[1]) > 0:
             low_end = min(self.select_range[0], self.select_range[1])
             high_end = max(self.select_range[0], self.select_range[1])
-            clipboard_copy(self.text_box_layout.plain_text[low_end:high_end])
+            if self.ui_manager.copy_text_enabled and self.copy_text_enabled:
+                clipboard_copy(self.text_box_layout.plain_text[low_end:high_end])
 
     def _calculate_double_click_word_selection(self):
         """
diff --git a/pygame_gui/elements/ui_text_entry_box.py b/pygame_gui/elements/ui_text_entry_box.py
index 6500e66..04262db 100644
--- a/pygame_gui/elements/ui_text_entry_box.py
+++ b/pygame_gui/elements/ui_text_entry_box.py
@@ -71,6 +71,7 @@ class UITextEntryBox(UITextBox):
         self.cursor_blink_delay_after_moving = 1.0
         self.blink_cursor_time_acc = 0.0
         self.blink_cursor_time = 0.4
+        self.paste_text_enabled = True
 
         self.cursor_on = False
 
@@ -333,17 +334,19 @@ class UITextEntryBox(UITextBox):
         return consumed_event
 
     def _do_cut(self):
-        if abs(self.select_range[0] - self.select_range[1]) > 0:
-            low_end = min(self.select_range[0], self.select_range[1])
-            high_end = max(self.select_range[0], self.select_range[1])
-            clipboard_copy(self.html_text[low_end:high_end])
-            self.text_box_layout.delete_selected_text()
-            self.edit_position = low_end
-            self.html_text = self.html_text[:low_end] + self.html_text[high_end:]
-            self.text_box_layout.set_cursor_position(self.edit_position)
-            self.select_range = [0, 0]
-            self.redraw_from_text_block()
-            self.cursor_has_moved_recently = True
+        if self.ui_manager.copy_text_enabled and self.copy_text_enabled:
+            if abs(self.select_range[0] - self.select_range[1]) > 0:
+                low_end = min(self.select_range[0], self.select_range[1])
+                high_end = max(self.select_range[0], self.select_range[1])
+
+                clipboard_copy(self.html_text[low_end:high_end])
+                self.text_box_layout.delete_selected_text()
+                self.edit_position = low_end
+                self.html_text = self.html_text[:low_end] + self.html_text[high_end:]
+                self.text_box_layout.set_cursor_position(self.edit_position)
+                self.select_range = [0, 0]
+                self.redraw_from_text_block()
+                self.cursor_has_moved_recently = True
 
     def _process_paste_event(self, event: Event) -> bool:
         """
@@ -362,30 +365,31 @@ class UITextEntryBox(UITextBox):
         return consumed_event
 
     def _do_paste(self):
-        paste = clipboard_paste()
-        if paste is not None:
-            new_text = self.convert_all_line_endings_to_unix(clipboard_paste())
+        if self.ui_manager.paste_text_enabled and self.paste_text_enabled:
+            paste = clipboard_paste()
+            if paste is not None:
+                new_text = self.convert_all_line_endings_to_unix(clipboard_paste())
 
-            if abs(self.select_range[0] - self.select_range[1]) > 0:
-                low_end = min(self.select_range[0], self.select_range[1])
-                high_end = max(self.select_range[0], self.select_range[1])
-                self.html_text = self.html_text[:low_end] + new_text + self.html_text[high_end:]
-                self.set_text(self.html_text)
-                self.edit_position = low_end + len(new_text)
-                self.text_box_layout.set_cursor_position(self.edit_position)
-                self.redraw_from_text_block()
-                self.select_range = [0, 0]
-                self.cursor_has_moved_recently = True
-            elif len(new_text) > 0:
-                self.html_text = (self.html_text[:self.edit_position] +
-                                  new_text +
-                                  self.html_text[self.edit_position:])
-                original_edit_pos = self.edit_position
-                self.set_text(self.html_text)
-                self.edit_position = original_edit_pos + len(new_text)
-                self.text_box_layout.set_cursor_position(self.edit_position)
-                self.redraw_from_text_block()
-                self.cursor_has_moved_recently = True
+                if abs(self.select_range[0] - self.select_range[1]) > 0:
+                    low_end = min(self.select_range[0], self.select_range[1])
+                    high_end = max(self.select_range[0], self.select_range[1])
+                    self.html_text = self.html_text[:low_end] + new_text + self.html_text[high_end:]
+                    self.set_text(self.html_text)
+                    self.edit_position = low_end + len(new_text)
+                    self.text_box_layout.set_cursor_position(self.edit_position)
+                    self.redraw_from_text_block()
+                    self.select_range = [0, 0]
+                    self.cursor_has_moved_recently = True
+                elif len(new_text) > 0:
+                    self.html_text = (self.html_text[:self.edit_position] +
+                                      new_text +
+                                      self.html_text[self.edit_position:])
+                    original_edit_pos = self.edit_position
+                    self.set_text(self.html_text)
+                    self.edit_position = original_edit_pos + len(new_text)
+                    self.text_box_layout.set_cursor_position(self.edit_position)
+                    self.redraw_from_text_block()
+                    self.cursor_has_moved_recently = True
 
     def redraw_from_text_block(self):
         self.text_box_layout.fit_layout_rect_height_to_rows()
diff --git a/pygame_gui/elements/ui_text_entry_line.py b/pygame_gui/elements/ui_text_entry_line.py
index eb5f6b5..0458d38 100644
--- a/pygame_gui/elements/ui_text_entry_line.py
+++ b/pygame_gui/elements/ui_text_entry_line.py
@@ -147,6 +147,9 @@ class UITextEntryLine(UIElement):
         self.forbidden_characters: Optional[List[str]] = None
         self.length_limit: Optional[int] = None
 
+        self.copy_text_enabled = True
+        self.paste_text_enabled = True
+
         self.rebuild_from_changed_theme_data()
 
     @property
@@ -780,20 +783,21 @@ class UITextEntryLine(UIElement):
         return consumed_event
 
     def _do_cut(self):
-        if abs(self.select_range[0] - self.select_range[1]) > 0:
-            low_end = min(self.select_range[0], self.select_range[1])
-            high_end = max(self.select_range[0], self.select_range[1])
-            clipboard_copy(self.text[low_end:high_end])
-            if self.drawable_shape is not None:
-                self.drawable_shape.text_box_layout.delete_selected_text()
-                self.drawable_shape.apply_active_text_changes()
-            self.edit_position = low_end
-            self.text = self.text[:low_end] + self.text[high_end:]
-            if self.drawable_shape is not None:
-                self.drawable_shape.text_box_layout.set_cursor_position(self.edit_position)
-                self.drawable_shape.apply_active_text_changes()
-            self.select_range = [0, 0]
-            self.cursor_has_moved_recently = True
+        if self.ui_manager.copy_text_enabled and self.copy_text_enabled:
+            if abs(self.select_range[0] - self.select_range[1]) > 0:
+                low_end = min(self.select_range[0], self.select_range[1])
+                high_end = max(self.select_range[0], self.select_range[1])
+                clipboard_copy(self.text[low_end:high_end])
+                if self.drawable_shape is not None:
+                    self.drawable_shape.text_box_layout.delete_selected_text()
+                    self.drawable_shape.apply_active_text_changes()
+                self.edit_position = low_end
+                self.text = self.text[:low_end] + self.text[high_end:]
+                if self.drawable_shape is not None:
+                    self.drawable_shape.text_box_layout.set_cursor_position(self.edit_position)
+                    self.drawable_shape.apply_active_text_changes()
+                self.select_range = [0, 0]
+                self.cursor_has_moved_recently = True
 
     def _process_copy_event(self, event: Event) -> bool:
         """
@@ -817,7 +821,8 @@ class UITextEntryLine(UIElement):
         if abs(self.select_range[0] - self.select_range[1]) > 0:
             low_end = min(self.select_range[0], self.select_range[1])
             high_end = max(self.select_range[0], self.select_range[1])
-            clipboard_copy(self.text[low_end:high_end])
+            if self.ui_manager.copy_text_enabled and self.copy_text_enabled:
+                clipboard_copy(self.text[low_end:high_end])
 
     def _process_paste_event(self, event: pygame.event.Event) -> bool:
         """
@@ -837,52 +842,53 @@ class UITextEntryLine(UIElement):
         return consumed_event
 
     def _do_paste(self):
-        new_text = clipboard_paste()
-        if self.validate_text_string(new_text):
-            if abs(self.select_range[0] - self.select_range[1]) > 0:
-                low_end = min(self.select_range[0], self.select_range[1])
-                high_end = max(self.select_range[0], self.select_range[1])
-                final_text = self.text[:low_end] + new_text + self.text[high_end:]
-                within_length_limit = True
-                if self.length_limit is not None and len(final_text) > self.length_limit:
-                    within_length_limit = False
-                if within_length_limit:
-                    self.text = final_text
-                    if self.drawable_shape is not None:
-                        self.drawable_shape.text_box_layout.delete_selected_text()
-                        self.drawable_shape.apply_active_text_changes()
-                    display_new_text = new_text
-                    if self.is_text_hidden:
-                        display_new_text = self.hidden_text_char * len(new_text)
-                    if self.drawable_shape is not None:
-                        self.drawable_shape.insert_text(display_new_text, low_end)
-                    self.edit_position = low_end + len(new_text)
-                    if self.drawable_shape is not None:
-                        self.drawable_shape.text_box_layout.set_cursor_position(
-                            self.edit_position)
-                        self.drawable_shape.apply_active_text_changes()
-                    self.select_range = [0, 0]
-                    self.cursor_has_moved_recently = True
-            elif len(new_text) > 0:
-                final_text = (self.text[:self.edit_position] +
-                              new_text +
-                              self.text[self.edit_position:])
-                within_length_limit = True
-                if self.length_limit is not None and len(final_text) > self.length_limit:
-                    within_length_limit = False
-                if within_length_limit:
-                    self.text = final_text
-                    display_new_text = new_text
-                    if self.is_text_hidden:
-                        display_new_text = self.hidden_text_char * len(new_text)
-                    if self.drawable_shape is not None:
-                        self.drawable_shape.insert_text(display_new_text, self.edit_position)
-                    self.edit_position += len(new_text)
-                    if self.drawable_shape is not None:
-                        self.drawable_shape.text_box_layout.set_cursor_position(
-                            self.edit_position)
-                        self.drawable_shape.apply_active_text_changes()
-                    self.cursor_has_moved_recently = True
+        if self.ui_manager.paste_text_enabled and self.paste_text_enabled:
+            new_text = clipboard_paste()
+            if self.validate_text_string(new_text):
+                if abs(self.select_range[0] - self.select_range[1]) > 0:
+                    low_end = min(self.select_range[0], self.select_range[1])
+                    high_end = max(self.select_range[0], self.select_range[1])
+                    final_text = self.text[:low_end] + new_text + self.text[high_end:]
+                    within_length_limit = True
+                    if self.length_limit is not None and len(final_text) > self.length_limit:
+                        within_length_limit = False
+                    if within_length_limit:
+                        self.text = final_text
+                        if self.drawable_shape is not None:
+                            self.drawable_shape.text_box_layout.delete_selected_text()
+                            self.drawable_shape.apply_active_text_changes()
+                        display_new_text = new_text
+                        if self.is_text_hidden:
+                            display_new_text = self.hidden_text_char * len(new_text)
+                        if self.drawable_shape is not None:
+                            self.drawable_shape.insert_text(display_new_text, low_end)
+                        self.edit_position = low_end + len(new_text)
+                        if self.drawable_shape is not None:
+                            self.drawable_shape.text_box_layout.set_cursor_position(
+                                self.edit_position)
+                            self.drawable_shape.apply_active_text_changes()
+                        self.select_range = [0, 0]
+                        self.cursor_has_moved_recently = True
+                elif len(new_text) > 0:
+                    final_text = (self.text[:self.edit_position] +
+                                  new_text +
+                                  self.text[self.edit_position:])
+                    within_length_limit = True
+                    if self.length_limit is not None and len(final_text) > self.length_limit:
+                        within_length_limit = False
+                    if within_length_limit:
+                        self.text = final_text
+                        display_new_text = new_text
+                        if self.is_text_hidden:
+                            display_new_text = self.hidden_text_char * len(new_text)
+                        if self.drawable_shape is not None:
+                            self.drawable_shape.insert_text(display_new_text, self.edit_position)
+                        self.edit_position += len(new_text)
+                        if self.drawable_shape is not None:
+                            self.drawable_shape.text_box_layout.set_cursor_position(
+                                self.edit_position)
+                            self.drawable_shape.apply_active_text_changes()
+                        self.cursor_has_moved_recently = True
 
     def _process_mouse_button_event(self, event: pygame.event.Event) -> bool:
         """
diff --git a/pygame_gui/ui_manager.py b/pygame_gui/ui_manager.py
index e53a42f..08802af 100644
--- a/pygame_gui/ui_manager.py
+++ b/pygame_gui/ui_manager.py
@@ -101,6 +101,9 @@ class UIManager(IUIManagerInterface):
         self.text_hovered = False
         self.hovering_any_ui_element = False
 
+        self.copy_text_enabled = True
+        self.paste_text_enabled = True
+
         if auto_load:
             self.resource_loader.start()
             # If we are using a blocking loader this will only return when loading is complete


Test patch:
diff --git a/tests/test_elements/test_ui_scrolling_container.py b/tests/test_elements/test_ui_scrolling_container.py
index 666045b..1fe3dca 100644
--- a/tests/test_elements/test_ui_scrolling_container.py
+++ b/tests/test_elements/test_ui_scrolling_container.py
@@ -179,7 +179,7 @@ class TestUIScrollingContainer:
 
         container.update(0.02)
 
-        assert container.get_container().get_relative_rect().y == -11
+        assert container.get_container().get_relative_rect().y == -12
 
         container.horiz_scroll_bar.scroll_wheel_moved = True
         container.horiz_scroll_bar.scroll_wheel_amount = -5.0