Repository: MyreMylar/pygame_gui (language: python)

Issue:
Add UIForm Element
Auto resizing containers added (Documentation on website is remaining)

These containers automatically resize when elements are added to them.

I was having some problems running tests, but I had tested them separately in a project I was using them in. More similar elements are coming like `UIAutoScrollingContainer `which uses an auto resizing container as its scrollable container instead of a normal `UIContainer`.
Check if you like this implementation, or maybe we could even merge `UIContainer `and the new `UIAutoResizingContainer`

**Adding PR split list to first post:**

**Easy changes**
1.  [x] Adding .idea to .gitignore PR - #530

**Changes to existing base classes**
1. [x] Changes being made to UIElement - This needs some merge effort with other recent changes, I had a stab locally but I think it can probably be done better. Also I need to interrogate what theses change are more fully as changes here can have a wide impact across the codebase as I discovered last week with a smaller change to dynamic resizing, that passed all the unit tests, managing to break an image resizing demo. #531, #534, #535, #536
2. [x] Changes to UIContainer - Same reasoning as above regarding wide ranging impact. #537
3. [x] Changes to UIScrollingContainer - I'm less concerned about changes here as it is not widely used, but it's worth looking at - there is also the possibility we may want to merge this class entirely with AutoScrollingContainer. #538, #539

**New stuff**
1. [x] AutoScrollingContainer #545
2. [x] AutoResizingContainer #543, #544
3. [x] UI2DSlider #540
4. [ ] UIForm

**Using the new stuff in existing stuff**
1. [x] UIColourPicker addition of UI2DSlider #542


Discussion / hints:
The main question I had when looking over this is - are there circumstances where you would want a container that resizes like this, as opposed to one that resizes it's scrollable area (e.g. the way an explorer or one where you have decided the size ahead of time to fit the stuff you are going to put inside of it? Most UIs I see these days adopt a 'squish and vertical scroll' for oversized content - I suspect mainly because of the dominance of phones and tablets in browsing content. 

I suspect there are - but listing off some ideas as to what this would be used for in a UI would probably help clarify the design and help create an example that uses this type of container for the examples repository:

https://github.com/MyreMylar/pygame_gui_examples
 
I'll admit that I didn't have too much in mind except it would be easy to replace the `UIContainer` used in the `UIScrollableContainer` to this special container. I thought we would have to implement something like this either straight into the `UIScrollableContainer` otherwise anyways, and implementing it this way would potentially allow for more use cases to be available with the same effort for maintenance.
I think the usage possibility would be when you'd require a container to extend to contained elements but not to the point where you'd need scrollbars, by restricting the `maximum_edges` rect appropriately.
So for example when you want to put a `UIContainer` inside a `UIScrollableContainer`, you wouldn't want the inner container to have scrollbars regardless of how large it becomes because then you'd end up having a container with scrollbars inside a container with scrollbars.
I understand that this implementation doesn't have _a ton_ of more added freedom of usage, but it still adds at least _some_ more freedom to use this feature and I don't see any major downsides to it
Hi,
Thank you for reviewing the pull request. I also found some flaws with this code when resizing the container. I think there are certain other major problems with the class which I am trying to pin point properly.
However, I had already finished most work on the uiscrolling container which automatically resizes, and some other elements.
Yet I cannot guarantee that I will have the time to fix all the issues as I have some important exams coming up which will last the whole of February. I will try my best to debug the problems and update the necessary code, but I think it might be better to have the other elements' code added. Currently, I have these elements (almost) finished:
* UIAutoScrollingContainer
* UI2DSlider
* UIColourPickerDialog (with an added 2Dslider in the colour square which allows dragging to select the correct colour) 
* UIForm

These elements aren't fully completed, and I'm sure there are bugs but you'll be able to see what my approach to those elements is and guide me if necessary. You may even finish it if I am too inactive. It will atleast get the process of adding those elements going
While renaming the `UIContainer` method `on_anchor_target_changed` to `on_content_moved_or_resized` as you suggested, I saw that the UIElement class has a method called `_on_content_changed` which has no documentation. What exactly does this method do? Perhaps some documentation with that would make it unambiguous about which function does what.

Secondly, the `get_top_most_point` is not necessarily the same as getting the `self.rect.top`, because `self.rect.top` will not be changed to the y value from `get_top_most_point` unless `resize_top` is `True`. This means that using this method could be way to check what size the container will be in the case where `resize_top` is `False` and they wish to toggle it to `True`.
However, I can add the prefix if you feel like that use case is too specific and may confuse the users.
I made some changes using your suggestions. However, I can just not get the container to work properly. It seems to be having some problems with buttons and their texts, and especially with dynamic width and heights.
I think I found the root of the problem. So what happens when we create a new button with dynamic width and height is:
1. First, it creates a full sized image containing the text.
2. Then it sees how much of the image fits in the container, and crops the image accordingly. 
3. *However, it also resizes its rect to the size of the image.*
4.  This means that since the AutoResizingContainer checks whether it needs to expand using the rect of the button, it fails to recognise that the button exceeds the borders.
5.  Now since the container doesn't resize, no matter how many times you try to resize the button, the button will always be invisible since the rect doesn't change, the clipping rect doesn't change and the image size doesn't change.

So, to fix this problem, we'll have to **stop the image from resizing based on if it's outside the container**. Just update the clipping rect instead and as soon as the element moves update the clipping rect, and only blit the the image actually contained within the container.
I finally was able to fix the problem by eleminating the if condition in `UIElement._set_image` which checks whether the `image_clipping_rect` is 0 and sets the `universal_emtpy_surface` as the `self.image`. 
Looks like a crop of the unit tests are failing now I've enabled them to run.
> Looks like a crop of the unit tests are failing now I've enabled them to run.

I have tried fixing some of the failing tests with the next commit, could you try re-running the workflow? it doesn't allow me to run it myself. Also, when I run the tests in my IDE, many of them fail because the manager can't find the specified files, or the colours of the gradient don't match, etc. I don't think I have changed any code which would interfere with the colours or location of files. There is a test which specifically tests for the if condition I had removed, where in the expected image was supposed to be the `ui_manager.universal_empty_surface` but after I changed it (so that the size of the image isn't 0, 0) the tests fails.

I will also add the appropriate tests in a few days
> > Looks like a crop of the unit tests are failing now I've enabled them to run.
> 
> I have tried fixing some of the failing tests with the next commit, could you try re-running the workflow? it doesn't allow me to run it myself. Also, when I run the tests in my IDE, many of them fail because the manager can't find the specified files, or the colours of the gradient don't match, etc. I don't think I have changed any code which would interfere with the colours or location of files. There is a test which specifically tests for the if condition I had removed, where in the expected image was supposed to be the `ui_manager.universal_empty_surface` but after I changed it (so that the size of the image isn't 0, 0) the tests fails.
> 
> I will also add the appropriate tests in a few days

I have kicked off the tests.

To run them locally you should just be able to run `pytest tests` from the root directory (the one that has the 'tests', 'docs' and 'pygame_gui' folders. For me it looks like this:

![image](https://github.com/MyreMylar/pygame_gui/assets/13382426/82a4ce19-915d-4be0-b73c-3ca23bfc108b)
Okay, I have finally seemed to squash the bugs coming up with the containers while resizing. Also, I added some other functions and removed other bugs with this, namely:
1. `UIContainer` now also has `expand_top` and `expand_left` functions which work like `set_dimensions` except, as the name suggests, they expand the container from the top and left respectively. The big difference between just setting the dimensions and moving the rect, and these functions is that these functions also update the relative rects of the contained elements. This means that none of the contained elements' absolute positions will move.
2. I updated the `on_contained_elements_changed` method to recursively update targets as when there was a chain of elements all anchored to each other, it would not update all of them previously

I ran all the tests and it seems to be working properly. I tested the elements manually and they are working but I have not written the pytest equivalent tests yet. It's a bit daunting of a task because there are so many things to test.
If you run the tests again they should all be successful
Looks like things are coming along. These are the failing tests at the minute:

```
FAILED tests/test_windows/test_ui_colour_picker_dialog.py::TestUIColourPickerDialog::test_creation - AttributeError: 'NoneType' object has no attribute 'width'
FAILED tests/test_windows/test_ui_colour_picker_dialog.py::TestUIColourPickerDialog::test_create_too_small - AttributeError: 'NoneType' object has no attribute 'width'
FAILED tests/test_windows/test_ui_colour_picker_dialog.py::TestUIColourPickerDialog::test_press_cancel_button - AttributeError: 'NoneType' object has no attribute 'width'
FAILED tests/test_windows/test_ui_colour_picker_dialog.py::TestUIColourPickerDialog::test_press_ok_button - AttributeError: 'NoneType' object has no attribute 'width'
FAILED tests/test_windows/test_ui_colour_picker_dialog.py::TestUIColourPickerDialog::test_click_in_saturation_value_square_button - AttributeError: 'NoneType' object has no attribute 'width'
FAILED tests/test_windows/test_ui_colour_picker_dialog.py::TestUIColourPickerDialog::test_mess_with_colour_channel_event - AttributeError: 'NoneType' object has no attribute 'width'
FAILED tests/test_windows/test_ui_colour_picker_dialog.py::TestUIColourPickerDialog::test_update_saturation_value_square - AttributeError: 'NoneType' object has no attribute 'width'
FAILED tests/test_windows/test_ui_colour_picker_dialog.py::TestUIColourPickerDialog::test_update_current_colour_image - AttributeError: 'NoneType' object has no attribute 'width'
FAILED tests/test_windows/test_ui_colour_picker_dialog.py::TestUIColourPickerDialog::test_changed_rgb_update_hsv - AttributeError: 'NoneType' object has no attribute 'width'
FAILED tests/test_windows/test_ui_colour_picker_dialog.py::TestUIColourPickerDialog::test_changed_hsv_update_rgb - AttributeError: 'NoneType' object has no attribute 'width'
FAILED tests/test_windows/test_ui_colour_picker_dialog.py::TestUIColourPickerDialog::test_show - AttributeError: 'UI2DSlider' object has no attribute 'sliding_button'
FAILED tests/test_windows/test_ui_colour_picker_dialog.py::TestUIColourPickerDialog::test_hide - AttributeError: 'NoneType' object has no attribute 'width'
FAILED tests/test_windows/test_ui_colour_picker_dialog.py::TestUIColourPickerDialog::test_show_hide_rendering - AttributeError: 'UI2DSlider' object has no attribute 'sliding_button'
```
And most of them look like the same fail there.
Ah, My "Fixed Potential Bug" commit is the culprit here. Added comments so that I don't unfix a bug again. However, 2 of the tests are still failling I'll see what is causing them

The tests were failing mainly because the `UIButton` in the `UI2DSlider` kept consuming the event so the colour wouldn't change. Usually this wouldn't be a problem because clicking the button in place shouldn't change the colour anyway, but because this was done by the program where the colour was set programmatically and the necessary values not updated properly, it would cause errors
Also, now there is a border around the sat square of the `UIColourPickerDialog` due to the `2DSlider`. You can decide whether you want that or not and set the border to transparent in the default file if you don't want it
One of the tests is failing but I can't figure out why. The changes I made really look right to me when I locally tested it, but I don't know which part of the code I changed is affecting the test and therefore whether that was by intention or not. Mysteriously, even though the test works with the horizontal scrollbar of the scrollable container, it doesn't with the vertical scrollbar
Why are the two scrollbars implemented a bit differently? could be something to do with it. I think I need your help debugging with this one, @MyreMylar because I can't even figure out what the bug is and whether it affects the container at all to the user
Aha! I have fixed the bugs. It had to do with the visible container being resized after calculations of the sizes of the scrollbars instead of before the calculations, as it was done before. I will start work on the tests, but some features remaining to add are:

1. Padding around auto resizing containers, because currently it is made so that it will fit perfectly to the elements inside clamped by its min and max edges rect, and being able to change that would help. but, I am not sure whether I should use the theming file or a value passed in during initialisation because I think other `IUIContainer` elements like `UIAutoScrollingContainer` and `UIForm` would like to use this functionality. If we use the theme file then I'm not sure how the other elements will use it.

2. Some more types of inputs in UIForm, like a colour input using colour pickers, or another type of field which are required to be input, and perhaps a password type input. I have created an issue, #526 for the second one. I have a way for the user of the library to create a questionnaire with required elements, but the actual presence check has not been implemented so it's currently not functional.

3. Validation of min and max edges rect in autoresizing containers to make sure that min edges rect is contained completely within max edges rect, otherwise raise a warning perhaps?

Fix patch:
diff --git a/pygame_gui/elements/__init__.py b/pygame_gui/elements/__init__.py
index 97c535a9..4338f3d8 100644
--- a/pygame_gui/elements/__init__.py
+++ b/pygame_gui/elements/__init__.py
@@ -18,6 +18,7 @@
 from pygame_gui.elements.ui_window import UIWindow
 from pygame_gui.elements.ui_scrolling_container import UIScrollingContainer
 from pygame_gui.elements.ui_text_entry_box import UITextEntryBox
+from pygame_gui.elements.ui_auto_resizing_container import UIAutoResizingContainer
 
 
 __all__ = ['UIImage',
@@ -40,4 +41,5 @@
            'UITextEntryLine',
            'UIWindow',
            'UIScrollingContainer',
-           'UITextEntryBox']
+           'UITextEntryBox',
+           'UIAutoResizingContainer']
diff --git a/pygame_gui/elements/ui_auto_resizing_container.py b/pygame_gui/elements/ui_auto_resizing_container.py
new file mode 100644
index 00000000..e9f8051b
--- /dev/null
+++ b/pygame_gui/elements/ui_auto_resizing_container.py
@@ -0,0 +1,330 @@
+import pygame
+from pygame_gui.core import UIElement, UIContainer, ObjectID
+from pygame_gui.core.interfaces import IUIElementInterface, IUIManagerInterface, IContainerLikeInterface
+
+from typing import *
+
+
+class UIAutoResizingContainer(UIContainer):
+    """
+    A container like UI element that updates its size as elements within it change size, or new elements are added
+
+    :param relative_rect: The starting size and relative position of the container.
+    :param min_edges_rect: The Rect which defines the maximum values for the left and top,
+     and the minimum values for the right and bottom edges of the container. Defaults to the None (current rect)
+    :param max_edges_rect: The Rect which defines the minimum values for the left and top,
+     and the maximum values for the right and bottom edges of the container. Defaults to the None (unbounded)
+    :param resize_left: Should the left side be resized?
+    :param resize_right: Should the right side be resized?
+    :param resize_top: Should the top side be resized?
+    :param resize_bottom: Should the bottom side be resized?
+    :param manager: The UI manager for this element. If not provided or set to None,
+                    it will try to use the first UIManager that was created by your application.
+    :param starting_height: The starting layer height of this container above its container.
+                            Defaults to 1.
+    :param container: The container this container is within. Defaults to None (which is the root
+                      container for the UI)
+    :param parent_element: A parent element for this container. Defaults to None, or the
+                           container if you've set that.
+    :param object_id: An object ID for this element.
+    :param anchors: Layout anchors in a dictionary.
+    :param visible: Whether the element is visible by default. Warning - container visibility
+                    may override this.
+    """
+
+    def __init__(self,
+                 relative_rect: pygame.Rect,
+                 min_edges_rect: pygame.Rect = None,
+                 max_edges_rect: pygame.Rect = None,
+                 resize_left: bool = True,
+                 resize_right: bool = True,
+                 resize_top: bool = True,
+                 resize_bottom: bool = True,
+                 manager: Optional[IUIManagerInterface] = None,
+                 *,
+                 starting_height: int = 1,
+                 container: Optional[IContainerLikeInterface] = None,
+                 parent_element: Optional[UIElement] = None,
+                 object_id: Optional[Union[ObjectID, str]] = None,
+                 anchors: Optional[Dict[str, Union[str, UIElement]]] = None,
+                 visible: int = 1):
+
+        super().__init__(relative_rect, manager,
+                         starting_height=starting_height,
+                         container=container,
+                         parent_element=parent_element,
+                         object_id=object_id,
+                         anchors=anchors,
+                         visible=visible,
+                         element_id=['scrolling_container']
+                         )
+
+        self.min_edges_rect = min_edges_rect
+        # TODO: Add validation for min and max edges rect
+
+        if self.min_edges_rect is None:
+            self.min_edges_rect = self.get_relative_rect().copy()
+
+        self.max_edges_rect = max_edges_rect
+
+        self.abs_min_edges_rect = None
+        self.abs_max_edges_rect = None
+        self.recalculate_abs_edges_rect()
+
+        self.resize_left = resize_left
+        self.resize_right = resize_right
+        self.resize_top = resize_top
+        self.resize_bottom = resize_bottom
+
+        # Elements which affect the corresponding edge of the container
+        self.left_element: Optional[UIElement] = None
+        self.right_element: Optional[UIElement] = None
+        self.top_element: Optional[UIElement] = None
+        self.bottom_element: Optional[UIElement] = None
+
+        # Right to left is not the inverse of left to right because of varying sizes of elements
+        self.left_to_right_elements: List[IUIElementInterface] = []
+        self.right_to_left_elements: List[IUIElementInterface] = []
+        self.top_to_bottom_elements: List[IUIElementInterface] = []
+        self.bottom_to_top_elements: List[IUIElementInterface] = []
+
+        self.should_update_sorting = False
+        self.should_update_dimensions = False
+
+        self.has_recently_updated_dimensions = False
+
+    def add_element(self, element: IUIElementInterface) -> None:
+        """
+        Add a UIElement to the container. The UIElement's relative_rect parameter will be relative to
+        this container. Overridden to also update dimensions.
+
+        :param element: A UIElement to add to this container.
+        :return: None
+        """
+        super().add_element(element)
+
+        self.should_update_sorting = True  # Currently, the rect is just a copy of relative rect so cannot do sorting
+        self.should_update_dimensions = True
+
+    def remove_element(self, element: IUIElementInterface) -> None:
+        """
+        Remove a UIElement from this container.
+
+        :param element: A UIElement to remove from this container.
+        :return: None
+        """
+        super().remove_element(element)
+
+        if element in self.left_to_right_elements:
+            self.left_to_right_elements.remove(element)
+
+        if element in self.right_to_left_elements:
+            self.right_to_left_elements.remove(element)
+
+        if element in self.top_to_bottom_elements:
+            self.top_to_bottom_elements.remove(element)
+
+        if element in self.bottom_to_top_elements:
+            self.bottom_to_top_elements.remove(element)
+
+        if element in (self.left_element, self.right_element, self.top_element, self.bottom_element):
+            self._update_extreme_elements()
+
+        self.should_update_dimensions = True
+
+    def _get_left_most_point(self) -> int:
+        """
+        Gets the minimum x value any element has clamped to the min_edges_rect and max_edges_rect
+        :return: An integer
+        """
+        left = self.abs_min_edges_rect.left
+        if self.left_element:
+            left = min(left, self.left_element.get_abs_rect().left)
+        if self.abs_max_edges_rect is not None:
+            left = max(left, self.abs_max_edges_rect.left)
+        return left
+
+    def _get_right_most_point(self) -> int:
+        """
+        Gets the maximum x value any element has clamped to the min_edges_rect and max_edges_rect
+        :return: An integer
+        """
+        right = self.abs_min_edges_rect.right
+        if self.right_element:
+            right = max(right, self.right_element.get_abs_rect().right)
+        if self.abs_max_edges_rect is not None:
+            right = min(right, self.abs_max_edges_rect.right)
+        return right
+
+    def _get_top_most_point(self) -> int:
+        """
+        Gets the minimum y value any element has clamped to the min_edges_rect and max_edges_rect
+        :return: An integer
+        """
+        top = self.abs_min_edges_rect.top
+        if self.top_element:
+            top = min(top, self.top_element.get_abs_rect().top)
+        if self.abs_max_edges_rect is not None:
+            top = max(top, self.abs_max_edges_rect.top)
+        return top
+
+    def _get_bottom_most_point(self) -> int:
+        """
+        Gets the maximum y value any element has clamped to the min_edges_rect and max_edges_rect
+        :return: An integer
+        """
+        bottom = self.abs_min_edges_rect.bottom
+        if self.bottom_element:
+            bottom = max(bottom, self.bottom_element.get_abs_rect().bottom)
+        if self.abs_max_edges_rect is not None:
+            bottom = min(bottom, self.abs_max_edges_rect.bottom)
+        return bottom
+
+    def recalculate_abs_edges_rect(self) -> None:
+        """
+        Used to recalculate the absolute rects from the min and max edges rect which control the minimum and maximum
+        sizes of the container. Usually called when the container of this container has moved, or the minimum or
+        maximum rects have changed.
+
+        :return: None
+        """
+        self.abs_min_edges_rect = self._calc_abs_rect_pos_from_rel_rect(relative_rect=self.min_edges_rect,
+                                                                        container=self.ui_container,
+                                                                        anchors=self.anchors)[0]
+
+        if self.max_edges_rect:
+            self.abs_max_edges_rect = self._calc_abs_rect_pos_from_rel_rect(relative_rect=self.min_edges_rect,
+                                                                            container=self.ui_container,
+                                                                            anchors=self.anchors)[0]
+
+    def update_containing_rect_position(self) -> None:
+        """
+        Overridden to also recalculate the absolute rects which control the minimum and maximum sizes of the container
+
+        :return: None
+        """
+        super().update_containing_rect_position()
+        self.recalculate_abs_edges_rect()
+
+    def update_min_edges_rect(self, new_rect: pygame.Rect) -> None:
+        """
+        Updates the container's maximum values for the left and top, and the minimum value for
+        the right and bottom edges based upon the edges of the new rect.
+
+        Call the update function if you want to update the elements contained in the same frame,
+        otherwise the elements contained within will update in the next frame.
+
+        :param new_rect: Rect to update the min_edges_rect with
+        :return: None
+        """
+        if not isinstance(new_rect, pygame.Rect):
+            raise ValueError("Argument passed is not a pygame.Rect object")
+        if new_rect != self.min_edges_rect:
+            self.min_edges_rect = new_rect
+            self.recalculate_abs_edges_rect()
+
+    def update_max_edges_rect(self, new_rect: pygame.Rect) -> None:
+        """
+        Updates the container's minimum values for the left and top, and the maximum value for
+        the right and bottom edges based upon the edges of the new rect.
+
+        Call the update function if you want to update the elements contained in the same frame,
+        otherwise the elements contained within will update in the next frame.
+
+        :param new_rect: Rect to update the max_edges_rect with
+        :return: None
+        """
+        if not isinstance(new_rect, pygame.Rect):
+            raise ValueError("Argument passed is not a pygame.Rect object")
+        if new_rect != self.max_edges_rect:
+            self.max_edges_rect = new_rect
+            self.recalculate_abs_edges_rect()
+
+    def _update_sorting(self) -> None:
+        """
+        Updates the sorting of the elements from left to right, top to bottom etc.
+
+        :return: None
+        """
+        elements = self.elements
+
+        self.left_to_right_elements = sorted(elements, key=(lambda element: element.get_abs_rect().left))
+        self.right_to_left_elements = sorted(elements, key=(lambda element: element.get_abs_rect().right), reverse=True)
+        self.top_to_bottom_elements = sorted(elements, key=(lambda element: element.get_abs_rect().top))
+        self.bottom_to_top_elements = sorted(elements, key=(lambda element: element.get_abs_rect().bottom),
+                                             reverse=True)
+
+    def _update_extreme_elements(self) -> None:
+        """
+        Updates which elements are currently responsible for preventing the container from collapsing on each side
+
+        :return: None
+        """
+
+        self.left_element = self.left_to_right_elements[0] if self.left_to_right_elements else None
+        self.right_element = self.right_to_left_elements[0] if self.right_to_left_elements else None
+        self.top_element = self.top_to_bottom_elements[0] if self.top_to_bottom_elements else None
+        self.bottom_element = self.bottom_to_top_elements[0] if self.bottom_to_top_elements else None
+
+    def _update_dimensions(self) -> None:
+        """
+        Updates the sizes of the container
+
+        :return: None
+        """
+        rect = self.get_abs_rect()
+
+        left_ext = rect.left - self._get_left_most_point() if self.resize_left else 0
+        right_ext = self._get_right_most_point() - rect.right if self.resize_right else 0
+        top_ext = rect.top - self._get_top_most_point() if self.resize_top else 0
+        bottom_ext = self._get_bottom_most_point() - rect.bottom if self.resize_bottom else 0
+
+        if left_ext:
+            self.expand_left(left_ext)
+        if top_ext:
+            self.expand_top(top_ext)
+
+        if right_ext or bottom_ext:
+            width = rect.width + right_ext
+            height = rect.height + bottom_ext
+            self.set_dimensions((width, height))
+
+    def on_contained_elements_changed(self, target: UIElement) -> None:
+        """
+        Update the positioning of the contained elements of this container. To be called when one of the contained
+        elements may have moved, been resized or changed its anchors.
+
+        :param target: the UI element that has been benn moved resized or changed its anchors.
+        :return: None
+        """
+        super().on_contained_elements_changed(target)
+
+        self._update_sorting()
+        self._update_extreme_elements()
+
+        self.should_update_dimensions = True
+
+    def update(self, time_delta: float) -> None:
+        """
+        Updates the container's size based upon the elements inside.
+
+        Call this function if you have added or removed an element from this container and want
+        to update the size in the same frame, otherwise it will update in the next frame.
+
+        :param time_delta: The time passed between frames, measured in seconds.
+        :return: None
+        """
+        super().update(time_delta)
+
+        if self.has_recently_updated_dimensions:
+            self.has_recently_updated_dimensions = False
+
+        if self.should_update_sorting:  # Only used when adding elements as their rects aren't accurate during creation
+            self._update_sorting()
+            self._update_extreme_elements()
+            self.should_update_sorting = False
+
+        if self.should_update_dimensions:
+            self._update_dimensions()
+            self.should_update_dimensions = False
+            self.has_recently_updated_dimensions = True


Test patch:
diff --git a/tests/test_elements/test_ui_auto_resizing_container.py b/tests/test_elements/test_ui_auto_resizing_container.py
new file mode 100644
index 00000000..9af5aacf
--- /dev/null
+++ b/tests/test_elements/test_ui_auto_resizing_container.py
@@ -0,0 +1,284 @@
+import pytest
+import pygame
+
+from pygame_gui.elements.ui_auto_resizing_container import UIAutoResizingContainer
+from pygame_gui.elements.ui_button import UIButton
+from pygame_gui.core.interfaces import IUIManagerInterface
+from pygame_gui.elements import UIVerticalScrollBar
+
+
+class TestUIContainer:
+    def test_creation(self, _init_pygame, default_ui_manager, _display_surface_return_none):
+        UIAutoResizingContainer(pygame.Rect(100, 100, 200, 200), manager=default_ui_manager)
+
+    def test_get_container(self, _init_pygame, default_ui_manager: IUIManagerInterface,
+                           _display_surface_return_none):
+        container = UIAutoResizingContainer(pygame.Rect(100, 100, 200, 200), manager=default_ui_manager)
+        assert container.get_container() == container
+
+    def test_add_element(self, _init_pygame, default_ui_manager: IUIManagerInterface,
+                         _display_surface_return_none):
+        container = UIAutoResizingContainer(pygame.Rect(100, 100, 200, 200), manager=default_ui_manager)
+
+        button = UIButton(relative_rect=pygame.Rect(0, 0, 50, 50), text="",
+                          manager=default_ui_manager)
+        default_ui_manager.get_root_container().remove_element(button)
+        container.add_element(button)
+        container.update(0.4)
+        assert len(container.elements) == 1
+
+        button_right = UIButton(relative_rect=pygame.Rect(300, 50, 50, 50), text="",
+                                manager=default_ui_manager)
+        default_ui_manager.get_root_container().remove_element(button_right)
+        container.add_element(button_right)
+        container.update(0.4)
+        assert len(container.elements) == 2
+
+        button_left = UIButton(relative_rect=pygame.Rect(-100, 50, 50, 50), text="",
+                               manager=default_ui_manager)
+        default_ui_manager.get_root_container().remove_element(button_left)
+        container.add_element(button_left)
+        container.update(0.4)
+        assert len(container.elements) == 3
+
+        button_top = UIButton(relative_rect=pygame.Rect(100, -50, 50, 50), text="",
+                              manager=default_ui_manager)
+        default_ui_manager.get_root_container().remove_element(button_top)
+        container.add_element(button_top)
+        container.update(0.4)
+        assert len(container.elements) == 4
+
+        button_bottom = UIButton(relative_rect=pygame.Rect(100, 400, 50, 50), text="",
+                                 manager=default_ui_manager)
+        default_ui_manager.get_root_container().remove_element(button_bottom)
+        container.add_element(button_bottom)
+        container.update(0.4)
+        assert len(container.elements) == 5
+
+    def test_remove_element(self, _init_pygame, default_ui_manager: IUIManagerInterface,
+                            _display_surface_return_none):
+        container = UIAutoResizingContainer(pygame.Rect(100, 100, 200, 200), manager=default_ui_manager)
+
+        button = UIButton(relative_rect=pygame.Rect(0, 0, 50, 50), text="",
+                          manager=default_ui_manager,
+                          container=container)
+
+        container.remove_element(button)
+
+        button_right = UIButton(relative_rect=pygame.Rect(300, 50, 50, 50), text="",
+                                manager=default_ui_manager,
+                                container=container)
+        container.remove_element(button_right)
+        button_left = UIButton(relative_rect=pygame.Rect(-100, 50, 50, 50), text="",
+                               manager=default_ui_manager,
+                               container=container)
+        container.remove_element(button_left)
+        button_top = UIButton(relative_rect=pygame.Rect(100, -50, 50, 50), text="",
+                              manager=default_ui_manager,
+                              container=container)
+        container.remove_element(button_top)
+        button_bottom = UIButton(relative_rect=pygame.Rect(100, 400, 50, 50), text="",
+                                 manager=default_ui_manager,
+                                 container=container)
+        container.remove_element(button_bottom)
+
+        container.update(0.4)
+        assert len(container.elements) == 0
+
+    def test_recalculate_container_layer_thickness(self, _init_pygame,
+                                                   default_ui_manager: IUIManagerInterface,
+                                                   _display_surface_return_none):
+        container = UIAutoResizingContainer(pygame.Rect(100, 100, 200, 200), manager=default_ui_manager)
+        UIButton(relative_rect=pygame.Rect(0, 0, 50, 50), text="",
+                 manager=default_ui_manager,
+                 container=container)
+
+        container.recalculate_container_layer_thickness()
+
+        assert container.layer_thickness == 2
+
+    def test_change_container_layer(self, _init_pygame, default_ui_manager,
+                                    _display_surface_return_none):
+        container = UIAutoResizingContainer(pygame.Rect(100, 100, 200, 200), manager=default_ui_manager)
+
+        UIButton(relative_rect=pygame.Rect(0, 0, 50, 50), text="",
+                 manager=default_ui_manager, container=container)
+        container.change_layer(2)
+        assert container.get_top_layer() == 4
+
+    def test_get_top_layer(self, _init_pygame, default_ui_manager,
+                           _display_surface_return_none):
+        container = UIAutoResizingContainer(pygame.Rect(100, 100, 200, 200), manager=default_ui_manager)
+
+        UIButton(relative_rect=pygame.Rect(0, 0, 50, 50), text="",
+                 manager=default_ui_manager, container=container)
+        assert container.get_top_layer() == 3
+
+    def test_update_containing_rect_position(self, _init_pygame, default_ui_manager,
+                                             _display_surface_return_none):
+        container = UIAutoResizingContainer(pygame.Rect(100, 100, 200, 200), manager=default_ui_manager)
+        container_2 = UIAutoResizingContainer(pygame.Rect(50, 50, 50, 50), manager=default_ui_manager,
+                                              container=container)
+
+        button = UIButton(relative_rect=pygame.Rect(20, 20, 30, 20), text="X",
+                          manager=default_ui_manager, container=container_2)
+
+        container.set_position((0, 0))
+
+        container_2.update_containing_rect_position()
+
+        assert button.rect.topleft == (70, 70)
+
+    def test_set_position(self, _init_pygame, default_ui_manager,
+                          _display_surface_return_none):
+        container = UIAutoResizingContainer(pygame.Rect(100, 100, 200, 200), manager=default_ui_manager)
+
+        container.set_position((50, 50))
+
+        assert container.rect.topleft == (50, 50)
+
+    def test_set_relative_position(self, _init_pygame, default_ui_manager,
+                                   _display_surface_return_none):
+        container = UIAutoResizingContainer(pygame.Rect(100, 100, 200, 200), manager=default_ui_manager)
+        container_2 = UIAutoResizingContainer(pygame.Rect(50, 50, 50, 50), manager=default_ui_manager,
+                                              container=container)
+
+        container_2.set_relative_position((25, 25))
+
+        assert container_2.rect.topleft == (125, 125)
+
+    def test_set_dimensions(self, _init_pygame, default_ui_manager,
+                            _display_surface_return_none):
+        container = UIAutoResizingContainer(pygame.Rect(100, 100, 200, 200), manager=default_ui_manager)
+
+        container.set_dimensions((50, 50))
+
+        assert container.rect.size == (50, 50)
+
+    def test_kill(self, _init_pygame, default_ui_manager,
+                  _display_surface_return_none):
+        container = UIAutoResizingContainer(pygame.Rect(100, 100, 200, 200), manager=default_ui_manager)
+        container_2 = UIAutoResizingContainer(pygame.Rect(50, 50, 50, 50), manager=default_ui_manager,
+                                              container=container)
+
+        button = UIButton(relative_rect=pygame.Rect(20, 20, 30, 20), text="X",
+                          manager=default_ui_manager, container=container_2)
+
+        container.kill()
+
+        assert not button.alive()
+        assert not container_2.alive()
+        assert not container.alive()
+
+    def test_clear(self, _init_pygame, default_ui_manager,
+                   _display_surface_return_none):
+        container = UIAutoResizingContainer(pygame.Rect(100, 100, 200, 200), manager=default_ui_manager)
+        container_2 = UIAutoResizingContainer(pygame.Rect(50, 50, 50, 50), manager=default_ui_manager,
+                                              container=container)
+
+        button = UIButton(relative_rect=pygame.Rect(20, 20, 30, 20), text="X",
+                          manager=default_ui_manager, container=container_2)
+
+        container.clear()
+
+        assert not button.alive()
+        assert not container_2.alive()
+        assert len(container.elements) == 0
+
+    def test_check_hover_when_not_able_to_hover(self, _init_pygame, default_ui_manager,
+                                                _display_surface_return_none):
+        container = UIAutoResizingContainer(pygame.Rect(100, 100, 200, 200), manager=default_ui_manager)
+        default_ui_manager.mouse_position = (150, 150)
+        assert container.check_hover(0.5, False) is True  # already hovering
+        container.kill()
+        assert container.check_hover(0.5, False) is False  # dead so can't hover anymore
+
+    def test_resizing_with_anchors(self, _init_pygame, default_ui_manager,
+                                   _display_surface_return_none):
+        container = UIAutoResizingContainer(relative_rect=pygame.Rect(0, 0, 300, 300),
+                                            manager=default_ui_manager)
+
+        scroll_bar = UIVerticalScrollBar(
+            relative_rect=pygame.Rect(-20, 0, 20, 300),
+            visible_percentage=0.5,
+            manager=default_ui_manager,
+            container=container,
+            anchors={'left': 'right',
+                     'right': 'right',
+                     'top': 'top',
+                     'bottom': 'bottom'})
+
+        assert scroll_bar.top_button.rect.width == 14
+        container.set_dimensions((400, 400))
+        assert scroll_bar.top_button.rect.width == 14
+
+    def test_container_show(self, _init_pygame, default_ui_manager: IUIManagerInterface,
+                            _display_surface_return_none):
+        container = UIAutoResizingContainer(pygame.Rect(100, 100, 200, 200), manager=default_ui_manager, visible=0)
+        assert container.visible == 0
+        container.show()
+        assert container.visible == 1
+
+    def test_container_hide(self, _init_pygame, default_ui_manager: IUIManagerInterface,
+                            _display_surface_return_none):
+        container = UIAutoResizingContainer(pygame.Rect(100, 100, 200, 200), manager=default_ui_manager, visible=1)
+        assert container.visible == 1
+        container.hide()
+        assert container.visible == 0
+
+    def test_container_children_inheriting_hidden_status(self, _init_pygame, default_ui_manager: IUIManagerInterface,
+                                                         _display_surface_return_none):
+        container = UIAutoResizingContainer(pygame.Rect(100, 100, 200, 200), manager=default_ui_manager, visible=0)
+        button = UIButton(relative_rect=pygame.Rect(0, 0, 50, 50), text="",
+                          manager=default_ui_manager, container=container, visible=1)
+        assert container.visible == 0
+        assert button.visible == 0
+
+    def test_hidden_container_children_behaviour_on_show(self, _init_pygame, default_ui_manager: IUIManagerInterface,
+                                                         _display_surface_return_none):
+        container = UIAutoResizingContainer(pygame.Rect(100, 100, 200, 200), manager=default_ui_manager, visible=0)
+        button = UIButton(relative_rect=pygame.Rect(0, 0, 50, 50), text="",
+                          manager=default_ui_manager, container=container)
+        assert container.visible == 0
+        assert button.visible == 0
+        container.show()
+        assert container.visible == 1
+        assert button.visible == 1
+
+    def test_visible_container_children_behaviour_on_show(self, _init_pygame, default_ui_manager: IUIManagerInterface,
+                                                          _display_surface_return_none):
+        container = UIAutoResizingContainer(pygame.Rect(100, 100, 200, 200), manager=default_ui_manager, visible=1)
+        button = UIButton(relative_rect=pygame.Rect(0, 0, 50, 50), text="",
+                          manager=default_ui_manager, container=container, visible=0)
+        assert container.visible == 1
+        assert button.visible == 0
+        container.show()
+        assert container.visible == 1
+        assert button.visible == 0
+
+    def test_visible_container_children_behaviour_on_hide(self, _init_pygame, default_ui_manager: IUIManagerInterface,
+                                                          _display_surface_return_none):
+        container = UIAutoResizingContainer(pygame.Rect(100, 100, 200, 200), manager=default_ui_manager, visible=1)
+        button = UIButton(relative_rect=pygame.Rect(0, 0, 50, 50), text="",
+                          manager=default_ui_manager, container=container)
+        assert container.visible == 1
+        assert button.visible == 1
+        container.hide()
+        assert container.visible == 0
+        assert button.visible == 0
+
+    def test_hidden_container_children_behaviour_on_hide(self, _init_pygame, default_ui_manager: IUIManagerInterface,
+                                                         _display_surface_return_none):
+        container = UIAutoResizingContainer(pygame.Rect(100, 100, 200, 200), manager=default_ui_manager, visible=0)
+        button = UIButton(relative_rect=pygame.Rect(0, 0, 50, 50), text="",
+                          manager=default_ui_manager, container=container)
+        button.show()
+        assert container.visible == 0
+        assert button.visible == 1
+        container.hide()
+        assert container.visible == 0
+        assert button.visible == 1
+
+
+if __name__ == '__main__':
+    pytest.console_main()