Repository: MyreMylar/pygame_gui
Pull request #551 (2024-03-29 17:37:35)

Enhancement: Add a new GUI Event:  UI_WINDOW_RESIZED
When creating UIWindows, I find I want to know if the user has resized the window. It currently looks like there are only two events related to UIWindow: UI_WINDOW_CLOSE, UI_WINDOW_MOVED_TO_FRONT, neither of which check for a resize. However UIWindows themselves handle resizing perfectly.

So this enhancement is to create a new GUI event: UI_WINDOW_RESIZED that is fired when a resize occurs.

I've added the new constant and updated the process_events() function to create a new GUI event and post it upon resizing. 

Documentation: 
> Fired when a window is resized.
> 
> 'type' : pygame_gui.UI_WINDOW_RESIZED,
> 'ui_element' : The UIWindow that fired this event.
> 'ui_object_id' : The most unique ID for the element that fired this event.
> 

Some example usage:
```
for event in pygame.event.get():
     if event.type == pygame_gui.UI_WINDOW_RESIZED:
         if event.ui_element == window:
             print("Window resized")
```

Let me know what else is needed!


Patch:
diff --git a/docs/source/events.rst b/docs/source/events.rst
index 39f04f9..6fe8b0b 100644
--- a/docs/source/events.rst
+++ b/docs/source/events.rst
@@ -319,9 +319,30 @@ clicked on by a user.
    :linenos:
 
     for event in pygame.event.get():
-        if event.type == pygame_gui.UI_WINDOW_CLOSE:
+        if event.type == pygame_gui.UI_WINDOW_MOVED_TO_FRONT:
             if event.ui_element == window:
-                print("Window closed")
+                print("Window moved to front")
+
+:class:`UIWindow <pygame_gui.elements.UIWindow>` - UI_WINDOW_RESIZED
+..................................................................
+
+Fired when a window is resized.
+
+ - **'type'** : pygame_gui.UI_WINDOW_RESIZED,
+ - **'ui_element'** : The :class:`UIWindow <pygame_gui.elements.UIWindow>` that fired this event.
+ - **'ui_object_id'** : The most unique ID for the element that fired this event.
+ - **'external_size'** : The total size of the window including title bar, borders & shadows.
+ - **'internal_size'** : The size inside the window where other elements are place (excluding title bar, borders & shadows).
+
+**Example usage**:
+
+.. code-block:: python
+   :linenos:
+
+    for event in pygame.event.get():
+        if event.type == pygame_gui.UI_WINDOW_RESIZED:
+            if event.ui_element == window:
+                print("Window resized")
 
 :class:`UIConfirmationDialog <pygame_gui.windows.UIConfirmationDialog>` - UI_CONFIRMATION_DIALOG_CONFIRMED
 ...........................................................................................................
@@ -444,4 +465,4 @@ Fired when a command is entered into a console window. Usually by typing it in a
         if event.type == pygame_gui.UI_TEXT_EFFECT_FINISHED:
             if event.ui_element == dialog_box:
                 if event.effect == pygame_gui.TEXT_EFFECT_FADE_OUT:
-                    print("Dialog faded out")
\ No newline at end of file
+                    print("Dialog faded out")
diff --git a/pygame_gui/__init__.py b/pygame_gui/__init__.py
index 867a55d..fc6befa 100644
--- a/pygame_gui/__init__.py
+++ b/pygame_gui/__init__.py
@@ -17,7 +17,7 @@ from pygame_gui._constants import UI_DROP_DOWN_MENU_CHANGED, UI_HORIZONTAL_SLIDE
 from pygame_gui._constants import UI_SELECTION_LIST_NEW_SELECTION
 from pygame_gui._constants import UI_SELECTION_LIST_DROPPED_SELECTION
 from pygame_gui._constants import UI_SELECTION_LIST_DOUBLE_CLICKED_SELECTION
-from pygame_gui._constants import UI_WINDOW_CLOSE, UI_WINDOW_MOVED_TO_FRONT
+from pygame_gui._constants import UI_WINDOW_CLOSE, UI_WINDOW_MOVED_TO_FRONT, UI_WINDOW_RESIZED
 from pygame_gui._constants import UI_CONFIRMATION_DIALOG_CONFIRMED
 from pygame_gui._constants import UI_FILE_DIALOG_PATH_PICKED, UI_COLOUR_PICKER_COLOUR_PICKED
 from pygame_gui._constants import UI_COLOUR_PICKER_COLOUR_CHANNEL_CHANGED
@@ -52,6 +52,7 @@ __all__ = ['UIManager',
            'UI_SELECTION_LIST_DOUBLE_CLICKED_SELECTION',
            'UI_WINDOW_CLOSE',
            'UI_WINDOW_MOVED_TO_FRONT',
+           "UI_WINDOW_RESIZED",
            'UI_CONFIRMATION_DIALOG_CONFIRMED',
            'UI_FILE_DIALOG_PATH_PICKED',
            'UI_COLOUR_PICKER_COLOUR_PICKED',
diff --git a/pygame_gui/_constants.py b/pygame_gui/_constants.py
index 11cc186..cc2c691 100644
--- a/pygame_gui/_constants.py
+++ b/pygame_gui/_constants.py
@@ -68,6 +68,7 @@ UI_SELECTION_LIST_DROPPED_SELECTION = custom_type()
 UI_SELECTION_LIST_DOUBLE_CLICKED_SELECTION = custom_type()
 UI_WINDOW_CLOSE = custom_type()
 UI_WINDOW_MOVED_TO_FRONT = custom_type()
+UI_WINDOW_RESIZED = custom_type()
 UI_CONFIRMATION_DIALOG_CONFIRMED = custom_type()
 UI_FILE_DIALOG_PATH_PICKED = custom_type()
 UI_COLOUR_PICKER_COLOUR_PICKED = custom_type()
@@ -251,6 +252,7 @@ __all__ = ['UI_BUTTON_PRESSED',
            'UI_SELECTION_LIST_DOUBLE_CLICKED_SELECTION',
            'UI_WINDOW_CLOSE',
            'UI_WINDOW_MOVED_TO_FRONT',
+           "UI_WINDOW_RESIZED",
            'UI_CONFIRMATION_DIALOG_CONFIRMED',
            'UI_FILE_DIALOG_PATH_PICKED',
            'UI_COLOUR_PICKER_COLOUR_PICKED',
diff --git a/pygame_gui/elements/ui_window.py b/pygame_gui/elements/ui_window.py
index 463289c..0a523f7 100644
--- a/pygame_gui/elements/ui_window.py
+++ b/pygame_gui/elements/ui_window.py
@@ -1,12 +1,12 @@
-from typing import Union, Tuple, Optional, List, Iterator
+from typing import Union, Optional, List, Iterator
 
 import pygame
 
-from pygame_gui._constants import UI_WINDOW_CLOSE, UI_WINDOW_MOVED_TO_FRONT, UI_BUTTON_PRESSED
+from pygame_gui._constants import UI_WINDOW_CLOSE, UI_WINDOW_MOVED_TO_FRONT, UI_WINDOW_RESIZED, UI_BUTTON_PRESSED
 from pygame_gui._constants import OldType
 
 from pygame_gui.core import ObjectID
-from pygame_gui.core.interfaces import IContainerLikeInterface, IUIContainerInterface
+from pygame_gui.core.interfaces import IContainerLikeInterface, IUIContainerInterface, Coordinate
 from pygame_gui.core.interfaces import IWindowInterface, IUIManagerInterface, IUIElementInterface
 from pygame_gui.core import UIElement, UIContainer
 from pygame_gui.core.drawable_shapes import RectDrawableShape, RoundedRectangleShape
@@ -49,14 +49,13 @@ class UIWindow(UIElement, IContainerLikeInterface, IWindowInterface):
         self.draggable = draggable
 
         self.edge_hovering = [False, False, False, False]
-        
-        if element_id is None:
-            element_ids = ['window']
-        elif isinstance(element_id, str):
+
+        element_ids = ['window']
+        if isinstance(element_id, str):
             element_ids = ['window', element_id]
         elif isinstance(element_id, list):
             element_ids = ['window'] + element_id
-        
+
         super().__init__(rect, manager, container=None,
                          starting_height=1,
                          layer_thickness=1,
@@ -106,10 +105,7 @@ class UIWindow(UIElement, IContainerLikeInterface, IWindowInterface):
         """
         self.is_blocking = state
 
-    def set_dimensions(self, dimensions: Union[pygame.math.Vector2,
-                                               Tuple[int, int],
-                                               Tuple[float, float]],
-                       clamp_to_container: bool = False):
+    def set_dimensions(self, dimensions: Coordinate, clamp_to_container: bool = False):
         """
         Set the size of this window and then re-sizes and shifts the contents of the windows
         container to fit the new size.
@@ -131,10 +127,14 @@ class UIWindow(UIElement, IContainerLikeInterface, IWindowInterface):
                 container_pos = (self.relative_rect.x + self.shadow_width,
                                  self.relative_rect.y + self.shadow_width)
                 self._window_root_container.set_relative_position(container_pos)
+                window_resize_event = pygame.event.Event(UI_WINDOW_RESIZED,
+                                                         {'ui_element': self,
+                                                          'ui_object_id': self.most_specific_combined_id,
+                                                          'external_size': self.rect.size,
+                                                          'internal_size': self.window_element_container.get_size()})
+                pygame.event.post(window_resize_event)
 
-    def set_relative_position(self, position: Union[pygame.math.Vector2,
-                                                    Tuple[int, int],
-                                                    Tuple[float, float]]):
+    def set_relative_position(self, position: Coordinate):
         """
         Method to directly set the relative rect position of an element.
 
@@ -148,9 +148,7 @@ class UIWindow(UIElement, IContainerLikeInterface, IWindowInterface):
                              self.relative_rect.y + self.shadow_width)
             self._window_root_container.set_relative_position(container_pos)
 
-    def set_position(self, position: Union[pygame.math.Vector2,
-                                           Tuple[int, int],
-                                           Tuple[float, float]]):
+    def set_position(self, position: Coordinate):
         """
         Method to directly set the absolute screen rect position of an element.
 
@@ -210,7 +208,7 @@ class UIWindow(UIElement, IContainerLikeInterface, IWindowInterface):
 
     def check_clicked_inside_or_blocking(self, event: pygame.event.Event) -> bool:
         """
-        A quick event check outside of the normal event processing so that this window is brought
+        A quick event check outside the normal event processing so that this window is brought
         to the front of the window stack if we click on any of the elements contained within it.
 
         :param event: The event to check.
@@ -278,7 +276,7 @@ class UIWindow(UIElement, IContainerLikeInterface, IWindowInterface):
 
     def _update_drag_resizing(self):
         """
-        Re-sizes a window that is being dragged around its the edges by the mouse.
+        Re-sizes a window that is being dragged around its edges by the mouse.
 
         """
         x_pos = self.rect.left
@@ -317,9 +315,9 @@ class UIWindow(UIElement, IContainerLikeInterface, IWindowInterface):
                 else:
                     x_pos = self.rect.left
         x_dimension = max(self.minimum_dimensions[0],
-                          min(self.ui_container.rect.width, x_dimension))
+                          min(self.ui_container.get_container().get_rect().width, x_dimension))
         y_dimension = max(self.minimum_dimensions[1],
-                          min(self.ui_container.rect.height, y_dimension))
+                          min(self.ui_container.get_container().get_rect().height, y_dimension))
         self.set_position((x_pos, y_pos))
         self.set_dimensions((x_dimension, y_dimension))
 
@@ -345,7 +343,7 @@ class UIWindow(UIElement, IContainerLikeInterface, IWindowInterface):
         For the window the only hovering we care about is the edges if this is a resizable window.
 
         :param time_delta: time passed in seconds between one call to this method and the next.
-        :param hovered_higher_element: Have we already hovered an element/window above this one.
+        :param hovered_higher_element: Have we already hovered an element/window above this one?
 
         """
         hovered = False
@@ -747,13 +745,12 @@ class UIWindow(UIElement, IContainerLikeInterface, IWindowInterface):
         rel_mouse_pos = None
         inside_window_rect = self.get_container().get_rect()
         if inside_window_rect.contains(pygame.Rect(abs_mouse_pos, (1, 1))):
-
             window_contents_top_left = inside_window_rect.topleft
             rel_mouse_pos = (abs_mouse_pos[0] - window_contents_top_left[0],
                              abs_mouse_pos[1] - window_contents_top_left[1])
 
         return rel_mouse_pos
-    
+
     def __iter__(self) -> Iterator[IUIElementInterface]:
         """
         Iterates over the elements within the container.


Test patch:
diff --git a/tests/test_elements/test_ui_window.py b/tests/test_elements/test_ui_window.py
index b52849b..bfc30f0 100644
--- a/tests/test_elements/test_ui_window.py
+++ b/tests/test_elements/test_ui_window.py
@@ -86,6 +86,22 @@ class TestUIWindow:
         assert button.rect.topright == (window.get_container().rect.right - 10,
                                         window.get_container().rect.top + 10)
         assert button.rect.topright == (474, 253)
+        
+        confirm_event_fired = False
+        event_object_id = None
+        new_external_dimensions = (0, 0)
+        new_internal_dimensions = (0, 0)
+        for event in pygame.event.get():
+            if (event.type == pygame_gui.UI_WINDOW_RESIZED and
+                    event.ui_element == window):
+                confirm_event_fired = True
+                event_object_id = event.ui_object_id
+                new_external_dimensions = event.external_size
+                new_internal_dimensions = event.internal_size
+        assert confirm_event_fired
+        assert event_object_id == 'window'
+        assert new_external_dimensions == (300, 400)
+        assert new_internal_dimensions == (268, 341)
 
     def test_set_relative_position(self, _init_pygame, default_ui_manager: IUIManagerInterface,
                                    _display_surface_return_none):