Repository: MyreMylar/pygame_gui
Pull request #494 (2024-01-15 13:38:39)

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


Patch:
diff --git a/pygame_gui/__init__.py b/pygame_gui/__init__.py
index fc6befa..035bf52 100644
--- a/pygame_gui/__init__.py
+++ b/pygame_gui/__init__.py
@@ -17,6 +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_FORM_SUBMITTED
 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
@@ -50,6 +51,7 @@ __all__ = ['UIManager',
            'UI_SELECTION_LIST_NEW_SELECTION',
            'UI_SELECTION_LIST_DROPPED_SELECTION',
            'UI_SELECTION_LIST_DOUBLE_CLICKED_SELECTION',
+           'UI_FORM_SUBMITTED',
            'UI_WINDOW_CLOSE',
            'UI_WINDOW_MOVED_TO_FRONT',
            "UI_WINDOW_RESIZED",
diff --git a/pygame_gui/_constants.py b/pygame_gui/_constants.py
index cc2c691..0586085 100644
--- a/pygame_gui/_constants.py
+++ b/pygame_gui/_constants.py
@@ -66,6 +66,7 @@ UI_2D_SLIDER_MOVED = custom_type()
 UI_SELECTION_LIST_NEW_SELECTION = custom_type()
 UI_SELECTION_LIST_DROPPED_SELECTION = custom_type()
 UI_SELECTION_LIST_DOUBLE_CLICKED_SELECTION = custom_type()
+UI_FORM_SUBMITTED = custom_type()
 UI_WINDOW_CLOSE = custom_type()
 UI_WINDOW_MOVED_TO_FRONT = custom_type()
 UI_WINDOW_RESIZED = custom_type()
@@ -250,6 +251,7 @@ __all__ = ['UI_BUTTON_PRESSED',
            'UI_SELECTION_LIST_NEW_SELECTION',
            'UI_SELECTION_LIST_DROPPED_SELECTION',
            'UI_SELECTION_LIST_DOUBLE_CLICKED_SELECTION',
+           'UI_FORM_SUBMITTED',
            'UI_WINDOW_CLOSE',
            'UI_WINDOW_MOVED_TO_FRONT',
            "UI_WINDOW_RESIZED",
diff --git a/pygame_gui/data/default_theme.json b/pygame_gui/data/default_theme.json
index 32abf3c..d0bd651 100644
--- a/pygame_gui/data/default_theme.json
+++ b/pygame_gui/data/default_theme.json
@@ -299,6 +299,56 @@
             "shadow_width": "1"
         }
     },
+    "section":
+    {
+        "prototype": "#default_shape_style",
+        "misc":
+        {
+            "padding": "5,5",
+            "label_height": "20",
+            "section_label_height": "30",
+            "field_height": "40",
+            "large_field_height": "80",
+            "selection_list_height": "-1",
+            "gap": "10",
+            "label_gap": "0",
+            "section_gap": "15",
+            "section_indent": "5"
+        }
+    },
+    "section.#expand_section_button":
+    {
+        "prototype": "#no_frills_button",
+        "font": {
+            "name": "fira_code_symbols",
+            "size": "14"
+        }
+    },
+    "form":
+    {
+        "prototype": "#default_shape_style",
+        "misc":
+        {
+            "padding": "5,5",
+            "label_height": "20",
+            "field_height": "40",
+            "large_field_height": "80",
+            "selection_list_height": "-1",
+            "gap": "10",
+            "label_gap": "0",
+            "section_gap": "15",
+            "section_indent": "5",
+            "submit_button_horiz_alignment": "right"
+        }
+    },
+    "form.#submit_button":
+    {
+        "prototype": "#no_frills_button",
+        "misc":
+        {
+            "text_horiz_alignment_padding": "5"
+        }
+    },
     "window":
     {
         "misc":
diff --git a/pygame_gui/elements/__init__.py b/pygame_gui/elements/__init__.py
index bb38e11..59cc7ff 100644
--- a/pygame_gui/elements/__init__.py
+++ b/pygame_gui/elements/__init__.py
@@ -20,6 +20,7 @@ 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
 from pygame_gui.elements.ui_tab_container import UITabContainer
+from pygame_gui.elements.ui_form import UIForm, InputField
 
 
 __all__ = ['UIImage',
@@ -44,5 +45,7 @@ __all__ = ['UIImage',
            'UIScrollingContainer',
            'UITextEntryBox',
            'UIAutoResizingContainer',
-           'UITabContainer' 
+           'UITabContainer',
+           'InputField',
+           'UIForm'
            ]
diff --git a/pygame_gui/elements/ui_auto_resizing_container.py b/pygame_gui/elements/ui_auto_resizing_container.py
index 92ccb6a..0181e50 100644
--- a/pygame_gui/elements/ui_auto_resizing_container.py
+++ b/pygame_gui/elements/ui_auto_resizing_container.py
@@ -198,6 +198,8 @@ class UIAutoResizingContainer(UIContainer):
                                                                             container=self.ui_container,
                                                                             anchors=self.anchors)[0]
 
+        self.should_update_dimensions = True
+
     def update_containing_rect_position(self) -> None:
         """
         Overridden to also recalculate the absolute rects which control the minimum and maximum sizes of the container
diff --git a/pygame_gui/elements/ui_form.py b/pygame_gui/elements/ui_form.py
new file mode 100644
index 0000000..fc1cf3a
--- /dev/null
+++ b/pygame_gui/elements/ui_form.py
@@ -0,0 +1,1299 @@
+import pygame
+
+from pygame_gui._constants import UI_BUTTON_PRESSED, UI_FORM_SUBMITTED
+from pygame_gui.core import UIElement, ObjectID
+from pygame_gui.core.interfaces import IUIElementInterface, IUIManagerInterface, IContainerLikeInterface
+from pygame_gui.core.drawable_shapes import RectDrawableShape, RoundedRectangleShape
+from pygame_gui.elements import UIAutoResizingContainer, UIScrollingContainer
+from pygame_gui.elements import UITextEntryLine, UITextEntryBox, UISelectionList, UIDropDownMenu, UILabel, UIButton
+
+import re
+from ast import literal_eval
+from typing import *
+
+
+class InputField(NamedTuple):
+    """
+    This NamedTuple is used within parsed questionnaires to store elements which take input, which were created from
+    default types supported by UIForm, like short_text, long_text etc.
+    """
+    question_type: str
+    label: UILabel
+    element: Union[UITextEntryLine, UITextEntryBox, UIDropDownMenu]
+
+
+class UISection(UIAutoResizingContainer):
+    """
+    A Section is a part of the UIForm which can contain questions or more sections. This behaviour is used to create
+    nested structures when required. A section can be expanded/contracted using a button created at initialisation.
+
+    :param relative_rect: The starting size and relative position of the section. The height will be calculated
+    automatically by the section. Therefore, any height passed in will have no effect on the section.
+    :param form: The UIForm element this section belongs to
+    :param name: The name of the section.
+    :param questionnaire: The section in the main questionnaire to handle.
+    :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,
+                 form: "UIForm",
+                 name: str,
+                 questionnaire: Mapping[str, Union[str, Mapping]],
+                 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,
+                         resize_left=False,
+                         resize_right=False,
+                         resize_top=False,
+                         manager=manager,
+                         starting_height=starting_height,
+                         container=container,
+                         parent_element=parent_element,
+                         object_id=object_id,
+                         anchors=anchors,
+                         visible=visible)
+
+        self._create_valid_ids(container=container,
+                               parent_element=parent_element,
+                               object_id=object_id,
+                               element_id="section")
+
+        self.form = form
+        self.name = name
+
+        self.background_colour = None
+        self.border_colour = None
+        self.background_image = None
+        self.border_width = 1
+        self.shadow_width = 2
+        self.padding = (5, 5)
+
+        self.label_height = 30
+        self.section_label_height = 30
+        self.field_height = 40
+        self.large_field_height = 80
+        self.selection_list_height = -1
+        self.gap = 10
+        self.label_gap = 0
+        self.section_gap = 15
+        self.section_indent = 5
+
+        self.rebuild_sub_elements = False
+
+        self.shape = "rectangle"
+        self.shape_corner_radius = 0
+
+        anchors = {"left": "left", "right": "right", "top": "top"}
+
+        # You can click this button to expand/hide the container
+        self.main_expand_button = UIButton(pygame.Rect(0, 0, 0, 0),
+                                           text=self.name + " ▼",
+                                           manager=manager,
+                                           container=self,
+                                           parent_element=parent_element,
+                                           object_id=ObjectID("#expand_section_button", None),
+                                           anchors=anchors.copy())
+
+        self.should_transition = False  # Should the section container switch from shown/hidden to the other state?
+
+        anchors = {**anchors,
+                   "top_target": self.main_expand_button}
+        # Container for the rest of the section
+        self.section_container = UIAutoResizingContainer(pygame.Rect(0, 0, self.relative_rect.width, 0),
+                                                         resize_left=False,
+                                                         resize_right=False,
+                                                         resize_top=False,
+                                                         # resize_bottom=True,
+                                                         manager=manager,
+                                                         container=self,
+                                                         parent_element=parent_element,
+                                                         object_id=ObjectID("#inner_section_container", None),
+                                                         anchors=anchors.copy()
+                                                         )
+        self.questionnaire = questionnaire
+        self.parsed_questionnaire = {}
+        self.parse_section()
+
+        self.rebuild_from_changed_theme_data()
+
+    def parse_section(self) -> None:
+        """
+        This function is used to create the ui elements to gather input based on the questionnaire passed.
+        See UIForm.validate_questionnaire for the structure of the questionnaire supported.
+
+        :return: None
+        """
+
+        # Main Expand Button
+
+        size = (self.relative_rect.width - 2 * self.padding[0], self.section_label_height)
+        self.main_expand_button.set_dimensions(size)
+        self.main_expand_button.set_relative_position((self.padding[0], 0))
+
+        anchors = {"left": "left", "right": "right", "top": "top"}
+
+        self.parsed_questionnaire = self.form.get_parsed_questionnaire(self, self.questionnaire,
+                                                                       self.ui_manager, self.section_container,
+                                                                       self, anchors)
+
+    def update(self, time_delta: float) -> None:
+        """
+        Called every update loop of our UI Manager. Handles the actual transitioning of the section from shown to hidden
+        and vice versa.
+
+        :param time_delta: The time in seconds between this update method call and the previous one.
+        :return: None
+        """
+        # Transition first, then resize so that the container size gets updates as soon as possible
+
+        container = self.section_container
+        if self.should_transition:
+            if container.visible:
+                # Hide the container and make the size 0 to avoid a giant void where the section used to be
+                container.hide()
+                container.set_dimensions((container.rect.width, 0))
+                container.resize_bottom = False  # Prevent it from resizing itself
+            else:
+                # Show the container. It will resize automatically when its update gets called.
+                container.show()
+                container.resize_bottom = True
+                container.should_update_dimensions = True
+
+            self.should_transition = False
+
+        super().update(time_delta)
+
+    def rebuild_from_changed_theme_data(self) -> None:
+        """
+        Called by the UIManager to check the theming data and rebuild whatever needs rebuilding
+        for this element when the theme data has changed.
+
+        :return: None
+        """
+        super().rebuild_from_changed_theme_data()
+        has_any_changed = False
+
+        background_colour = self.ui_theme.get_colour_or_gradient("dark_bg",
+                                                                 self.combined_element_ids)
+        if background_colour != self.background_colour:
+            self.background_colour = background_colour
+            has_any_changed = True
+
+        border_colour = self.ui_theme.get_colour_or_gradient("normal_border",
+                                                             self.combined_element_ids)
+        if border_colour != self.border_colour:
+            self.border_colour = border_colour
+            has_any_changed = True
+
+        # misc
+        if self._check_misc_theme_data_changed(attribute_name="shape",
+                                               default_value="rectangle",
+                                               casting_func=str,
+                                               allowed_values=["rectangle",
+                                                               "rounded_rectangle"]):
+            has_any_changed = True
+
+        if self._check_shape_theming_changed(defaults={"border_width": 1,
+                                                       "shadow_width": 2,
+                                                       "shape_corner_radius": 2}):
+            has_any_changed = True
+
+        if self._check_misc_theme_data_changed(attribute_name="padding",
+                                               default_value=(5, 5),
+                                               casting_func=self.tuple_extract):
+            has_any_changed = True
+            self.rebuild_sub_elements = True
+
+        if self._check_misc_theme_data_changed(attribute_name="label_height",
+                                               default_value=30,
+                                               casting_func=int):
+            has_any_changed = True
+            self.rebuild_sub_elements = True
+
+        if self._check_misc_theme_data_changed(attribute_name="section_label_height",
+                                               default_value=30,
+                                               casting_func=int):
+            has_any_changed = True
+            self.rebuild_sub_elements = True
+
+        if self._check_misc_theme_data_changed(attribute_name="field_height",
+                                               default_value=40,
+                                               casting_func=int):
+            has_any_changed = True
+            self.rebuild_sub_elements = True
+
+        if self._check_misc_theme_data_changed(attribute_name="large_field_height",
+                                               default_value=80,
+                                               casting_func=int):
+            has_any_changed = True
+            self.rebuild_sub_elements = True
+
+        if self._check_misc_theme_data_changed(attribute_name="selection_list_height",
+                                               default_value=-1,
+                                               casting_func=int):
+            has_any_changed = True
+            self.rebuild_sub_elements = True
+
+        if self._check_misc_theme_data_changed(attribute_name="gap",
+                                               default_value=10,
+                                               casting_func=int):
+            has_any_changed = True
+            self.rebuild_sub_elements = True
+
+        if self._check_misc_theme_data_changed(attribute_name="label_gap",
+                                               default_value=0,
+                                               casting_func=int):
+            has_any_changed = True
+            self.rebuild_sub_elements = True
+
+        if self._check_misc_theme_data_changed(attribute_name="section_gap",
+                                               default_value=15,
+                                               casting_func=int):
+            has_any_changed = True
+            self.rebuild_sub_elements = True
+
+        if self._check_misc_theme_data_changed(attribute_name="section_indent",
+                                               default_value=5,
+                                               casting_func=int):
+            has_any_changed = True
+            self.rebuild_sub_elements = True
+
+        if has_any_changed:
+            self.rebuild()
+
+    def rebuild(self) -> None:
+        """
+        A complete rebuild of the drawable shape used by this element.
+
+        :return: None
+        """
+
+        super().rebuild()
+
+        if self.rebuild_sub_elements:
+            self.form.rebuild_parsed_questionnaire(self, self.parsed_questionnaire)
+            self.rebuild_sub_elements = False
+
+        size = (self.relative_rect.width - 2 * self.padding[0], self.section_label_height)
+        self.main_expand_button.set_dimensions(size)
+        self.main_expand_button.set_relative_position((self.padding[0], 0))
+
+        theming_parameters = {"normal_bg": self.background_colour,
+                              "normal_border": self.border_colour,
+                              "normal_image": self.background_image,
+                              "border_width": self.border_width,
+                              "shadow_width": self.shadow_width,
+                              "shape_corner_radius": self.shape_corner_radius}
+
+        if self.shape == "rectangle":
+            self.drawable_shape = RectDrawableShape(self.rect, theming_parameters,
+                                                    ["normal"], self.ui_manager)
+        elif self.shape == "rounded_rectangle":
+            self.drawable_shape = RoundedRectangleShape(self.rect, theming_parameters,
+                                                        ["normal"], self.ui_manager)
+
+        self.on_fresh_drawable_shape_ready()
+
+    def process_event(self, event: pygame.event.Event) -> bool:
+        """
+        Handles the expansion and contraction of the section
+
+        :param event: The event to process
+        :return: Whether the event was consumed
+        """
+
+        if event.type == UI_BUTTON_PRESSED and event.mouse_button == pygame.BUTTON_LEFT:
+            if event.ui_element == self.main_expand_button:
+                self.should_transition = True
+        return False  # Should the section expansion/contraction events be consumed?
+
+
+class UIForm(UIScrollingContainer):
+    """
+    UIForm is used to create basic forms which can contain text fields, text boxes, dropdowns and more. The form can be
+    divided in sections. A section can be expanded/contracted using a button. It also creates a submit button which will
+    raise a UI_FORM_SUBMITTED event when clicked. The questions to the form should be parsed in the form of a
+    dictionary.
+
+    The questionnaire should be in the following format:
+
+    questionnaire = {
+        "section1": {"question1":"short_text", "question2":"boolean(default=False)", ...}, \n
+        ..., \n
+        "question3": UIDropDownMenu(...), \n
+        ...
+    }
+
+    Where each key and value represent either:
+
+    * A section's name and all questions within, or
+    * A question's name and its corresponding type (see supported values below)
+
+    Each section will create a sub-form for the questions within.
+
+     Supported types (with the elements created automatically to gather input):
+
+    * character (UITextEntryLine with 1 letter input)
+    * short_text (normal UITextEntryLine input)
+    * long_text (UITextEntryBox)
+    * integer (UITextEntryLine with only numeric inputs allowed)
+    * decimal (UITextEntryLine with numeric inputs and decimal point allowed).
+    **IMPORTANT: Any number of decimal points are currently allowed in this form of input.
+    If a value is entered which cannot be converted to a decimal, then 0.0 will be returned.**
+
+    * password (UITextEntryLine with hidden characters)
+    * boolean (UIDropDownMenu with True and False as the options. Default option is True)
+    * boolean(default=False) (UIDropDownMenu with True and False as the options and the default option as *default*)
+    The value of default must be either True or False. The types must be passed as a string only.
+    Type values are **not** case-sensitive.
+
+     Passing arguments for supported types:
+
+    Each type can have arguments which follow syntax similar to normal functions as shown above with the boolean
+    type. Currently, the only supported parameter is default. All string values should be passed in quotes, like:
+    short_text(default="Hello World")
+
+     You may provide an element instead of a pre-existing type. This element will be used for input.
+
+    Supported elements:
+
+    * UITextEntryLine
+    * UITextEntryBox
+    * UISelectionList
+    * UIDropDownMenu
+
+    Other elements (they must be instances of IUIElementInterface or its subclasses) may be passed in,
+    if they implement either:
+
+    * A get_current_input() function which takes no arguments and returns the currently input values; or
+    * A current_input attribute or property which holds the currently input values
+
+    If they do not implement any of the above, then this class will not handle its input, and you'll have to check
+    for the input of the element once the form has been submitted.
+    If they implement both, then get_current_input() will be used.
+
+    **NOTE: All elements passed in will automatically be repositioned, resized, and re-anchored automatically.
+    Their container will be set to the scrollable container of the UIForm.**
+
+    :param relative_rect: The starting size and relative position of the form.
+    :param questionnaire: The questionnaire is a mapping which contains the questions in the form in a specific format.
+    :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.
+    """
+    # TODO: Implement a show password button
+    # TODO: Implement colour inputs
+    # TODO: Implement required input
+    SUPPORTED_TYPES = {"character": {"default": "'.?'", "required": "True|False"},
+                       "short_text": {"default": "'.*'", "required": "True|False"},
+                       "long_text": {"default": "'.*'", "required": "True|False"},
+                       "password": {"default": "'.*'", "required": "True|False"},
+                       "integer": {"default": r"\d*", "required": "True|False"},
+                       "decimal": {"default": r"\d*[.]?\d*", "required": "True|False"},
+                       "boolean": {"default": "True|False", "required": "True|False"}
+                       }
+
+    def __init__(self,
+                 relative_rect: pygame.Rect,
+                 questionnaire: Optional[Mapping[str, Union[str, Mapping, IUIElementInterface]]] = None,
+                 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,
+                         allow_scroll_x=False,
+                         manager=manager,
+                         starting_height=starting_height,
+                         container=container,
+                         parent_element=parent_element,
+                         object_id=object_id,
+                         anchors=anchors,
+                         visible=visible)
+
+        self._create_valid_ids(container=container,
+                               parent_element=parent_element,
+                               object_id=object_id,
+                               element_id="form")
+
+        self.background_colour = None
+        self.border_colour = None
+        self.background_image = None
+        self.border_width = 1
+        self.shadow_width = 2
+        self.padding = (5, 5)  # TODO: Implement vertical padding from the bottom
+
+        self.label_height = 30
+        self.field_height = 40
+        self.large_field_height = 80
+        self.selection_list_height = -1
+        self.gap = 10
+        self.label_gap = 0
+        self.section_gap = 15
+        self.section_indent = 5
+
+        self.rebuild_sub_elements = False
+
+        self.shape = "rectangle"
+        self.shape_corner_radius = 0
+
+        self.submit_button_horiz_alignment = "right"
+
+        self.questionnaire = questionnaire or {}  # If None is passed, use an empty dictionary instead
+        self.parsed_questionnaire = {}
+
+        self.submit_button = UIButton(pygame.Rect(0, 0, -1, self.field_height),
+                                      text="Submit",
+                                      manager=manager,
+                                      container=self.scrollable_container,
+                                      parent_element=parent_element,
+                                      object_id=ObjectID("#submit_button", None))
+
+        self.parse_form()
+
+        self.rebuild_from_changed_theme_data()
+
+    def _get_current_values(self, dic: Mapping[str, Union[IUIElementInterface, Mapping]]) -> Dict:
+        """
+        This function is used by get_current_values to recursively replace the nested dictionary's copy's keys with the
+        values from the appropriate UIElements.
+
+        :param dic: The nested dictionary
+        :return: The copy of the nested mapping with values reflecting the values input by the user
+        """
+        value_dic = {}
+        for key, value in dic.items():
+            # Used for re-casting values from str to int/decimal/boolean
+            type_name = ""
+            if isinstance(value, InputField):
+                type_name = value.question_type
+                value = value.element  # This is because it includes both the label and the actual element we want.
+
+            if isinstance(value, UISection):
+                value_dic[key] = self._get_current_values(value.parsed_questionnaire)
+
+            elif isinstance(value, IUIElementInterface):
+
+                if isinstance(value, UITextEntryLine):
+                    text = value.get_text()
+                    zero_text = False
+
+                    if type_name in ["integer", "decimal"]:
+                        if text:
+                            try:
+                                if type_name == "integer":
+                                    text = int(text)
+                                else:
+                                    text = float(text)
+                            except ValueError:
+                                zero_text = True
+                        else:
+                            zero_text = True
+
+                    # If no text was entered or value could not be converted to integer or float then default to 0.
+                    if zero_text:
+                        if type_name == "integer":
+                            text = 0
+                        elif type_name == "decimal":
+                            text = 0.0
+
+                    value_dic[key] = text
+
+                elif isinstance(value, UITextEntryBox):
+                    value_dic[key] = value.get_text()
+
+                elif isinstance(value, UISelectionList):
+                    if not value.allow_multi_select:
+                        value_dic[key] = value.get_single_selection()
+                    else:
+                        value_dic[key] = value.get_multi_selection()
+
+                elif isinstance(value, UIDropDownMenu):
+                    selected_option = value.selected_option[0]
+                    if type_name == "boolean":
+                        selected_option = literal_eval(selected_option)
+                    value_dic[key] = selected_option
+
+                elif hasattr(value, "get_current_input") and isinstance(value.get_current_input, Callable):
+                    value_dic[key] = value.get_current_input()
+
+                elif hasattr(value, "current_input"):
+                    value_dic[key] = value.current_input
+
+                else:
+                    value_dic[key] = value
+
+        return value_dic
+
+    def get_current_values(self) -> Dict:
+        """
+        This function is used to get all the values from the input elements. If a UIElement is not supported, then the
+        UIElement is returned instead
+
+        :return: Dictionary with values reflecting the values input by the user
+        """
+        return self._get_current_values(self.parsed_questionnaire)
+
+    @staticmethod
+    def get_parsed_questionnaire(element: Union["UIForm", UISection], questionnaire: Mapping[str, Union[str, Mapping]],
+                                 manager: IUIManagerInterface,
+                                 container: IContainerLikeInterface, parent_element: UIElement,
+                                 anchors: Dict[str, Union[str, UIElement]]) -> \
+            Dict[str, Union[UISection, InputField, IUIElementInterface]]:
+        """
+        This function is used by UIForm.parse_form to recursively create the ui elements to gather input based
+        on the questionnaire passed. This prevents duplication of code as UISection elements created also call this
+        function
+
+        :param element: The element the questionnaire belongs to. Used for theming
+        :param questionnaire: The dict representing the questionnaire to parse
+        :param manager: The UIManager which will handle the elements
+        :param container: The container which should contain the elements
+        :param parent_element: The parent element
+        :param anchors: The starting anchors of the elements
+        :return: The parsed questionnaire
+        """
+        parsed_questionnaire = {}
+
+        form = element if isinstance(element, UIForm) else element.form
+        label_height = element.label_height
+        field_height = element.field_height
+        large_field_height = element.large_field_height
+        selection_list_height = element.selection_list_height
+        gap = element.gap
+        label_gap = element.label_gap
+        section_gap = element.section_gap
+        section_indent = element.section_indent
+
+        if element == form:
+            width = form.scrollable_container.relative_rect.width - 2 * element.padding[0]
+        else:
+            width = element.relative_rect.width - 2 * element.padding[0]
+        rel_rect = pygame.Rect(*element.padding,
+                               width,
+                               field_height)
+
+        for key, value in questionnaire.items():
+
+            param_dict = {"relative_rect": rel_rect.copy(),
+                          "manager": manager,
+                          "container": container,
+                          "parent_element": parent_element,
+                          "anchors": anchors.copy()}
+
+            if isinstance(value, Mapping):
+                # Create Section
+
+                param_dict["relative_rect"].width -= section_indent
+                param_dict["relative_rect"].x += section_indent
+                parsed_questionnaire[key] = UISection(form=form, name=key, questionnaire=value,
+                                                      object_id=ObjectID(f"#{key.lower()}", None),
+                                                      **param_dict)
+                rel_rect.y = section_gap
+
+            elif isinstance(value, str):
+                # Parse supported types to appropriate UIElements. Could use a separate function for this in the future
+
+                # Supported types (Pasted here for quick reference):
+                # character (UITextEntryLine with 1 letter input)
+                # short_text (normal UITextEntryLine input)
+                # long_text (UITextEntryBox)
+                # integer (UITextEntryLine with only numeric inputs allowed)
+                # decimal (UITextEntryLine with numeric inputs and decimal point allowed)
+                # boolean (UIDropDownMenu with True and False as the options. Default option is True)
+
+                type_name, args = UIForm.type_checker(value)
+
+                # Two elements are created, a label and the actual UIElement needed for input
+
+                param_dict["relative_rect"].width = -1
+                param_dict["relative_rect"].height = label_height
+
+                # This is to prevent labels from resizing and becoming too small to hold their text
+                right = param_dict["anchors"].pop("right", None)
+
+                label = UILabel(text=key, object_id=ObjectID(f"#{key.lower()}", "@form_label"), **param_dict)
+
+                param_dict["anchors"]["top_target"] = label
+                if right:
+                    param_dict["anchors"]["right"] = right
+                param_dict["relative_rect"] = rel_rect.copy()
+                param_dict["relative_rect"].y = label_gap
+                param_dict["relative_rect"].height = field_height
+                param_dict["object_id"] = ObjectID(f"#{key.lower()}", "@form_field")
+
+                element = None
+
+                if type_name == "character":
+                    element = UITextEntryLine(initial_text=str(args.get("default", "")), **param_dict)
+                    element.set_text_length_limit(1)
+
+                elif type_name == "short_text":
+                    element = UITextEntryLine(initial_text=str(args.get("default", "")), **param_dict)
+
+                elif type_name == "long_text":
+                    param_dict["relative_rect"].height = large_field_height
+                    element = UITextEntryBox(initial_text=str(args.get("default", "")), **param_dict)
+
+                elif type_name == "password":
+                    element = UITextEntryLine(initial_text=str(args.get("default", "")), **param_dict)
+                    element.set_text_hidden(True)
+
+                elif type_name == "integer":
+                    element = UITextEntryLine(initial_text=str(args.get("default", "")), **param_dict)
+                    element.set_allowed_characters("numbers")
+
+                elif type_name == "decimal":
+                    element = UITextEntryLine(initial_text=str(args.get("default", "")), **param_dict)
+                    element.set_allowed_characters([".", *list("0123456789")])
+
+                elif type_name == "boolean":
+                    element = UIDropDownMenu(options_list=["True", "False"],
+                                             starting_option=str(args.get("default", "True")),
+                                             **param_dict)
+                rel_rect.y = gap
+                parsed_questionnaire[key] = InputField(type_name, label, element)
+                anchors["top_target"] = element
+                continue
+
+            elif isinstance(value, IUIElementInterface):
+
+                value.ui_container.remove_element(value)
+                container = param_dict["container"].get_container()
+                container.add_element(value)
+                value.ui_container = container
+
+                value.set_anchors(anchors)
+                rect = rel_rect.copy()
+                value.set_dimensions((rect.width, rect.height))
+                value.set_relative_position(rect.topleft)
+
+                # TODO: Remove when UIElements get a set_container method eventually? Or perhaps the
+                #  list_and_scroll_bar_container should be contained within the UISelectionList?
+                if isinstance(value, UISelectionList):
+                    rect.height = selection_list_height
+                    if selection_list_height == -1:
+                        rect.height = value.total_height_of_list + (value.border_width + value.shadow_width) * 2
+
+                    value.set_dimensions((rect.width, rect.height))
+
+                    list_container = value.list_and_scroll_bar_container
+
+                    list_container.ui_container.remove_element(list_container)
+                    container.add_element(list_container)
+                    list_container.ui_container = container
+
+                    list_container.set_anchors(anchors)
+
+                # TODO: Remove when UIDropDownMenu does this automatically eventually?
+                if isinstance(value, UIDropDownMenu):
+                    value.current_state.update_position()
+                    value.current_state.update_dimensions()
+
+                rel_rect.y = gap
+                parsed_questionnaire[key] = value
+
+            anchors["top_target"] = parsed_questionnaire[key]
+
+        return parsed_questionnaire
+
+    def parse_form(self) -> None:
+        """
+        This function is used to create the ui elements to gather input based on the questionnaire passed. First the
+        questionnaire is validated. See UIForm.validate_questionnaire for the structure of the questionnaire supported
+
+        :return: None
+        """
+
+        self.validate_questionnaire(self.questionnaire, raise_error=True)
+
+        anchors = {"left": "left", "right": "right", "top": "top"}
+
+        self.parsed_questionnaire = self.get_parsed_questionnaire(self, self.questionnaire, self.ui_manager,
+                                                                  self.scrollable_container, self, anchors)
+
+        # Submit Button
+
+        if "top_target" in anchors:
+            self.submit_button.anchors["top_target"] = anchors["top_target"]
+            self.submit_button.relative_rect.y = self.gap
+        self.rebuild_submit_button()
+
+    def rebuild_from_changed_theme_data(self) -> None:
+        """
+        Called by the UIManager to check the theming data and rebuild whatever needs rebuilding
+        for this element when the theme data has changed.
+
+        :return: None
+        """
+        super().rebuild_from_changed_theme_data()
+        has_any_changed = False
+        rebuild_submit_button = False
+
+        background_colour = self.ui_theme.get_colour_or_gradient("dark_bg",
+                                                                 self.combined_element_ids)
+        if background_colour != self.background_colour:
+            self.background_colour = background_colour
+            has_any_changed = True
+
+        border_colour = self.ui_theme.get_colour_or_gradient("normal_border",
+                                                             self.combined_element_ids)
+        if border_colour != self.border_colour:
+            self.border_colour = border_colour
+            has_any_changed = True
+
+        # misc
+        if self._check_misc_theme_data_changed(attribute_name="shape",
+                                               default_value="rectangle",
+                                               casting_func=str,
+                                               allowed_values=["rectangle",
+                                                               "rounded_rectangle"]):
+            has_any_changed = True
+
+        if self._check_shape_theming_changed(defaults={"border_width": 1,
+                                                       "shadow_width": 2,
+                                                       "shape_corner_radius": 2}):
+            has_any_changed = True
+
+        if self._check_misc_theme_data_changed(attribute_name="padding",
+                                               default_value=(5, 5),
+                                               casting_func=self.tuple_extract):
+            has_any_changed = True
+            self.rebuild_sub_elements = True
+
+        if self._check_misc_theme_data_changed(attribute_name="label_height",
+                                               default_value=30,
+                                               casting_func=int):
+            has_any_changed = True
+            self.rebuild_sub_elements = True
+
+        if self._check_misc_theme_data_changed(attribute_name="field_height",
+                                               default_value=40,
+                                               casting_func=int):
+            has_any_changed = True
+            self.rebuild_sub_elements = True
+
+        if self._check_misc_theme_data_changed(attribute_name="large_field_height",
+                                               default_value=80,
+                                               casting_func=int):
+            has_any_changed = True
+            self.rebuild_sub_elements = True
+
+        if self._check_misc_theme_data_changed(attribute_name="selection_list_height",
+                                               default_value=-1,
+                                               casting_func=int):
+            has_any_changed = True
+            self.rebuild_sub_elements = True
+
+        if self._check_misc_theme_data_changed(attribute_name="gap",
+                                               default_value=10,
+                                               casting_func=int):
+            has_any_changed = True
+            self.rebuild_sub_elements = True
+
+        if self._check_misc_theme_data_changed(attribute_name="label_gap",
+                                               default_value=0,
+                                               casting_func=int):
+            has_any_changed = True
+            self.rebuild_sub_elements = True
+
+        if self._check_misc_theme_data_changed(attribute_name="section_gap",
+                                               default_value=15,
+                                               casting_func=int):
+            has_any_changed = True
+            self.rebuild_sub_elements = True
+
+        if self._check_misc_theme_data_changed(attribute_name="section_indent",
+                                               default_value=5,
+                                               casting_func=int):
+            has_any_changed = True
+            self.rebuild_sub_elements = True
+
+        if self._check_misc_theme_data_changed(attribute_name="submit_button_horiz_alignment",
+                                               default_value="right",
+                                               casting_func=str,
+                                               allowed_values=["left",
+                                                               "center",
+                                                               "right"]):
+            rebuild_submit_button = True
+
+        if has_any_changed:
+            self.rebuild()
+
+        if rebuild_submit_button:
+            self.rebuild_submit_button()
+
+    @staticmethod
+    def rebuild_parsed_questionnaire(element: Union["UIForm", UISection],
+                                     questionnaire: Mapping[str, Union[str, Mapping]]) -> None:
+
+        """
+        Called by UIForm.rebuild() to rebuild the elements of the form when theming elements like gaps between elements,
+        indentation of the sections, etc. change.
+
+        :param element: The element whose questionnaire to rebuild.
+        :param questionnaire: The dict representing the parsed_questionnaire to rebuild
+        :return: None
+        """
+
+        form = element if isinstance(element, UIForm) else element.form
+        label_height = element.label_height
+        field_height = element.field_height
+        large_field_height = element.large_field_height
+        selection_list_height = element.selection_list_height
+        gap = element.gap
+        label_gap = element.label_gap
+        section_gap = element.section_gap
+        section_indent = element.section_indent
+
+        if element == form:
+            width = form.scrollable_container.relative_rect.width - 2 * element.padding[0]
+        else:
+            width = element.relative_rect.width - 2 * element.padding[0]
+        relative_rect = pygame.Rect(*element.padding,
+                                    width,
+                                    field_height)
+
+        for key, value in questionnaire.items():
+
+            rel_rect = relative_rect.copy()
+
+            if isinstance(value, UISection):
+                # Update Section
+
+                rel_rect.width -= section_indent
+                rel_rect.x += section_indent
+
+                value.set_dimensions((rel_rect.width, rel_rect.height))
+                value.set_relative_position(rel_rect.topleft)
+
+                value.rebuild()
+                relative_rect.y = section_gap
+
+            elif isinstance(value, InputField):
+                # Updated appropriate UIElements for supported types.
+                # Could use a separate function for this in the future
+
+                # Supported types (Pasted here for quick reference):
+                # character (UITextEntryLine with 1 letter input)
+                # short_text (normal UITextEntryLine input)
+                # long_text (UITextEntryBox)
+                # integer (UITextEntryLine with only numeric inputs allowed)
+                # decimal (UITextEntryLine with numeric inputs and decimal point allowed)
+                # boolean (UIDropDownMenu with True and False as the options. Default option is True)
+
+                # Two elements are updated, a label and the actual UIElement needed for input
+
+                label = value.label
+                rel_rect.width = label.relative_rect.width
+                rel_rect.height = label_height
+
+                label.set_dimensions((rel_rect.width, rel_rect.height))
+                label.set_relative_position(rel_rect.topleft)
+
+                rel_rect = relative_rect.copy()
+                rel_rect.y = label_gap
+                rel_rect.height = field_height
+
+                type_name = value.question_type
+                element = value.element
+
+                if type_name == "character":
+                    element.set_text_length_limit(1)
+
+                elif type_name == "long_text":
+                    rel_rect.height = large_field_height
+
+                elif type_name == "password":
+                    element.set_text_hidden(True)
+
+                elif type_name == "integer":
+                    element.set_allowed_characters("numbers")
+
+                elif type_name == "decimal":
+                    element.set_allowed_characters([".", *list("0123456789")])
+
+                element.set_dimensions((rel_rect.width, rel_rect.height))
+                element.set_relative_position(rel_rect.topleft)
+
+                relative_rect.y = gap
+
+            elif isinstance(value, IUIElementInterface):
+
+                value.set_dimensions((rel_rect.width, rel_rect.height))
+                value.set_relative_position(rel_rect.topleft)
+
+                if isinstance(value, UISelectionList):
+                    rel_rect.height = selection_list_height
+                    if selection_list_height == -1:
+                        rel_rect.height = value.total_height_of_list + (value.border_width + value.shadow_width) * 2
+                    value.set_dimensions((rel_rect.width, rel_rect.height))
+
+                # TODO: Remove when UIDropDownMenu does this automatically eventually?
+                if isinstance(value, UIDropDownMenu):
+                    value.current_state.update_position()
+                    value.current_state.update_dimensions()
+
+                relative_rect.y = gap
+
+    def rebuild_submit_button(self) -> None:
+        """
+        Called by UIForm.rebuild() to rebuild the submit button based on its horizontal alignment
+
+        :return: None
+        """
+
+        anchors = {"top": "top"}
+        if "top_target" in self.submit_button.anchors:
+            anchors["top_target"] = self.submit_button.anchors["top_target"]
+
+        x_padding = self.padding[0]
+        pos = pygame.Vector2(0, self.submit_button.relative_rect.y)
+
+        if self.submit_button_horiz_alignment == "left":
+            anchors["left"] = "left"
+            pos.x = x_padding
+        elif self.submit_button_horiz_alignment == "center":
+            anchors["centerx"] = "centerx"
+        elif self.submit_button_horiz_alignment == "right":
+            anchors["right"] = "right"
+            pos.x = -self.submit_button.rect.width - x_padding
+
+        self.submit_button.set_anchors(anchors)
+        self.submit_button.set_dimensions((self.submit_button.relative_rect.width, self.field_height))
+        self.submit_button.set_relative_position(pos)
+
+    def rebuild(self) -> None:
+        """
+        A complete rebuild of the drawable shape used by this element.
+
+        :return: None
+        """
+
+        super().rebuild()
+
+        root_rect = pygame.Rect(self.relative_rect.x + self.shadow_width + self.border_width,
+                                self.relative_rect.y + self.shadow_width + self.border_width,
+                                self.relative_rect.width - (2 * self.shadow_width) - (2 * self.border_width),
+                                self.relative_rect.height - (2 * self.shadow_width) - (2 * self.border_width))
+
+        if self._root_container.relative_rect != root_rect:
+            self._root_container.set_relative_position(root_rect.topleft)
+            self._root_container.set_dimensions(root_rect.size)
+            self._calculate_scrolling_dimensions()
+            self._sort_out_element_container_scroll_bars()
+            self.rebuild_sub_elements = True
+
+        if self.rebuild_sub_elements:
+            self.rebuild_parsed_questionnaire(self, self.parsed_questionnaire)
+            self.rebuild_sub_elements = False
+
+        theming_parameters = {"normal_bg": self.background_colour,
+                              "normal_border": self.border_colour,
+                              "normal_image": self.background_image,
+                              "border_width": self.border_width,
+                              "shadow_width": self.shadow_width,
+                              "shape_corner_radius": self.shape_corner_radius}
+
+        if self.shape == "rectangle":
+            self.drawable_shape = RectDrawableShape(self.rect, theming_parameters,
+                                                    ["normal"], self.ui_manager)
+        elif self.shape == "rounded_rectangle":
+            self.drawable_shape = RoundedRectangleShape(self.rect, theming_parameters,
+                                                        ["normal"], self.ui_manager)
+
+        self.on_fresh_drawable_shape_ready()
+
+        # TODO: Remove when resizing bugs are fixed
+        self.scrollable_container.update_min_edges_rect(self.scrollable_container.relative_rect)
+
+    def process_event(self, event: pygame.event.Event) -> bool:
+        """
+        Handles the submission of the form
+
+        :param event: The event to process
+        :return: Whether the event was consumed
+        """
+
+        consumed = False
+        if event.type == UI_BUTTON_PRESSED and event.mouse_button == pygame.BUTTON_LEFT:
+            if event.ui_element == self.submit_button:
+                form_values = self.get_current_values()
+
+                # new event
+                event_data = {"form_values": form_values,
+                              "ui_element": self,
+                              "ui_object_id": self.most_specific_combined_id}
+                pygame.event.post(pygame.event.Event(UI_FORM_SUBMITTED, event_data))
+
+                consumed = True
+
+        return consumed
+
+    @staticmethod
+    def type_checker(string: str) -> Tuple[str, Dict[str, Any]]:
+        """
+        This is used to check whether a string can be used in a questionnaire to represent a type. Raises an appropriate
+        exception (either a NameError, SyntaxError or ValueError) if the string is not valid. Supported types are:
+
+        * character (UITextEntryLine with 1 letter input)
+        * short_text (normal UITextEntryLine input)
+        * long_text (UITextEntryBox)
+        * integer (UITextEntryLine with only numeric inputs allowed)
+        * decimal (UITextEntryLine with numeric inputs and decimal point allowed)
+        * password (UITextEntryLine input with characters hidden)
+        * boolean (UIDropDownMenu with True and False as the options. Default option is True)
+
+        :param string: The string to check
+        :return: A tuple of the type name with a list of argument-value tuples if the string is valid
+        """
+
+        string = string.strip()
+        # Type with their args and supported values for args
+        supported_types = UIForm.SUPPORTED_TYPES
+
+        type_check = re.compile(f"(?P<type_name>{'|'.join(supported_types)})(\\s)*(?:\\((?P<type_args>.*)\\))?",
+                                re.IGNORECASE | re.DOTALL)
+        type_name_match = type_check.fullmatch(string)
+        if not type_name_match:
+            raise ValueError(f"Question type '{string}' is not supported")
+
+        if string.endswith("()"):
+            raise SyntaxError(f"Parenthesis used without passing parameters for question")
+
+        type_name = type_name_match.group("type_name").lower()
+        params = supported_types[type_name]
+        arg_group = type_name_match.group("type_args")
+
+        if not arg_group:
+            return type_name, {}
+
+        arg_group.strip()
+
+        # Split the arguments passed based on commas. This is actually a more complicated process than you'd expect,
+        # because some commas might be contained in quotes. But we can't just check that the number of ' and " quotes
+        # are both even on each side because some ' might be contained within "" (for example in use of apostrophes) and
+        # some " might be contained within ''.
+
+        args = []
+        in_quotes = False
+        quotes_type = None
+        # Used to raise more informative errors when quotes are used in keywords and
+        # also for allowing whitespace between quotes in the same argument values, for e.g. "short_text('abc' 'def')"
+        was_in_quotes = False
+        arg_start_i = 0
+        keyword_present = False
+        keyword = None
+        arg_value_start_i = 0
+
+        if arg_group[-1] != ",":
+            # This is needed to ensure last argument passed is added to the args list
+            arg_group += ","
+
+        # TODO: Implement support for brackets
+        # TODO: Implement f strings, r strings eventually maybe
+        # TODO: Update docs about whitespace rules
+        for i, char in enumerate(arg_group):
+
+            if not in_quotes:
+                if char.isspace():
+                    # Whitespace is allowed only in certain places. These are:
+                    # Before type name (It is stripped above)
+                    # In between the type name and parenthesis (handled during regex matching)
+                    # After first parenthesis (It is stripped above)
+                    # Before keywords
+                    # Before '=' symbols
+                    # Before values for arguments
+                    # In values encased in quotes (Ignored due to parent if statement above)
+                    # In values between quotes (Only " ", \t and \f allowed)
+                    # Before final parenthesis (It is stripped above)
+                    # After final parenthesis (It is stripped above)
+                    if i == arg_start_i:
+                        arg_start_i += 1
+                        # We don't know yet whether a positional or keyword argument is being processed
+                        arg_value_start_i += 1
+                    elif not keyword_present:
+                        # Reaching here implies being between keyword and '=' symbols
+                        # unless there is a space in the middle of the keyword or value.
+                        next_char = arg_group[i + 1]
+                        if not next_char.isspace() and next_char not in "=," and not (
+                                next_char in "'\"" and was_in_quotes):
+                            raise SyntaxError(f"Found whitespace between keyword or value")
+
+                    elif keyword_present and i == arg_value_start_i:
+                        arg_value_start_i += 1
+
+                elif char == "=":
+                    if i == 0:
+                        raise SyntaxError("Found '=' at the beginning of arguments")
+                    if i == arg_start_i:
+                        raise SyntaxError("No keyword passed for argument before '=' symbol")
+                    if was_in_quotes:
+                        raise SyntaxError("Found keyword wrapped in quotes")
+                    if keyword_present:
+                        # Found 2 "=" symbols in the same argument: incorrect syntax
+                        raise SyntaxError(f"Found 2 consecutive '=' symbols")
+                    else:
+                        keyword_present = True
+                        keyword_end_i = i - 1
+                        arg_value_start_i = i + 1
+                        keyword = arg_group[arg_start_i: keyword_end_i + 1].strip()
+
+                elif char == ",":
+                    if i == 0:
+                        # Comma found at the beginning of arguments
+                        raise SyntaxError("Found comma at the beginning of arguments")
+                    if i == arg_start_i:
+                        # Two commas found consecutively
+                        raise SyntaxError("Found 2 consecutive commas")
+                    if keyword_present and i == arg_value_start_i:
+                        # No value passed after = sign
+                        raise SyntaxError(f"No value passed for keyword argument '{keyword}'")
+
+                    value = arg_group[arg_value_start_i: i].strip()
+                    try:
+                        value = literal_eval(value)
+                    except (ValueError, SyntaxError):
+                        # Literal_eval couldn't pass it as a str, int, bool etc.
+                        raise NameError(f"'{value}' is not defined. Perhaps quotes were missed")
+
+                    args.append((keyword, value))
+                    arg_start_i = i + 1
+                    keyword_present = False
+                    keyword = None
+                    arg_value_start_i = i + 1
+                    was_in_quotes = False
+
+                elif char == "'":
+                    in_quotes = True
+                    quotes_type = "'"
+
+                elif char == '"':
+                    in_quotes = True
+                    quotes_type = '"'
+
+            elif char == quotes_type:
+                in_quotes = False
+                quotes_type = None
+                was_in_quotes = True
+
+            elif i == len(arg_group) - 1:
+                # Last quotes symbol was unmatched
+                raise SyntaxError(f"Found unmatched {quotes_type}")
+
+        # Length of arguments supplied should be smaller than total params
+        if len(args) > len(params):
+            raise ValueError(f"Question of type '{type_name}' take at most {len(params)} arguments"
+                             f" but {len(args)} arguments were passed")
+
+        # Check that positional arguments don't follow keyword arguments
+        if any(args[i][0] and not args[i + 1][0] for i in range(len(args) - 1)):
+            raise SyntaxError("Found positional argument after keyword argument")
+
+        names_checked = []
+        # Evaluate whether each argument is valid
+        for i, (name, value) in enumerate(args):
+            if not name:
+                name = tuple(params)[i]
+                args[i] = name, args[i][1]
+            if name not in params:
+                raise ValueError(f"Unknown parameter '{name}' for question type '{type_name}'")
+            if name in names_checked:
+                raise SyntaxError(f"Got multiple values for parameter '{name}'")
+            if isinstance(value, str):
+                value = f"'{value}'"
+            if not re.fullmatch(params[name], str(value)):
+                raise ValueError(f"Invalid value '{value}' for argument '{name}'")
+            names_checked.append(name)
+
+        return type_name, dict(args)
+
+    @staticmethod
+    def validate_questionnaire(questionnaire: Mapping[str, Union[str, Mapping]],
+                               raise_error: bool = False) -> Optional[bool]:
+        """
+        Use this to validate if a questionnaire can be passed to the __init__ function. The format of the questionnaire
+        is given in the main documentation of the class.
+
+        :param questionnaire: A dictionary containing the details of the questionnaire in the appropriate format
+        :param raise_error: Should an error be raised with appropriate message? Default is False
+        :return: True or False
+        """
+
+        if not isinstance(questionnaire, Mapping):
+            if raise_error:
+                raise TypeError(f"Expected type 'Mapping' (Dict) for questionnaire, found type '{type(questionnaire)}'")
+            else:
+                return False
+
+        for key, value in questionnaire.items():
+            if not isinstance(key, str):
+                if raise_error:
+                    raise TypeError(f"Expected type 'str' for key in questionnaire, found type '{type(key)}'")
+                else:
+                    return False
+
+            # Sub-questionnaire
+            if isinstance(value, Mapping):
+                if not UIForm.validate_questionnaire(value, raise_error):
+                    return False
+
+            # Value is one of the supported types, check for validity
+            elif isinstance(value, str):
+                if raise_error:
+                    UIForm.type_checker(value)
+                else:
+                    try:
+                        UIForm.type_checker(value)
+                    except (NameError, SyntaxError, ValueError):
+                        return False
+
+            elif isinstance(value, IUIElementInterface):
+                continue
+
+            # Value not present
+            elif not value:
+                if raise_error:
+                    raise ValueError(f"Question type cannot be {value}")
+                else:
+                    return False
+
+            # Type of Value is not supported
+            else:
+                if raise_error:
+                    raise ValueError(f"Questions of type {type(value)} are not supported")
+                else:
+                    return False
+
+        return True


Test patch:
diff --git a/tests/test_elements/test_ui_form.py b/tests/test_elements/test_ui_form.py
new file mode 100644
index 0000000..7e260e4
--- /dev/null
+++ b/tests/test_elements/test_ui_form.py
@@ -0,0 +1,665 @@
+import os
+
+import pytest
+import pygame
+
+import pygame_gui
+from pygame_gui.elements.ui_form import InputField, UISection, UIForm
+from pygame_gui.elements import UITextEntryLine, UITextEntryBox, UISelectionList, UIDropDownMenu, UIButton
+from pygame_gui.core.interfaces import IUIManagerInterface
+from pygame_gui.ui_manager import UIManager
+from tests.shared_comparators import compare_surfaces
+
+
+def get_questionnaire():
+    test_rect = pygame.Rect(100, 100, 100, 100)
+    questionnaire = {
+        "Character Test:": "character",
+        "Short Text Test:": "short_text",
+        "Long Text Test:": "long_text",
+        "Integer Test:": "integer",
+        "Decimal Test:": "decimal",
+        "Password Test:": "password",
+        "Boolean Test:": "boolean",
+
+        "Section Test:": {
+            "UITextEntryLine Test": UITextEntryLine(test_rect),
+            "UITextEntryBox Test": UITextEntryBox(test_rect),
+            "UISelectionList Test": UISelectionList(test_rect, ["item 1", "item 2"]),
+            "UISelectionList Multiselect Test": UISelectionList(test_rect, ["item 1", "item 2"], allow_multi_select=True),
+            "UIDropDownMenu Test": UIDropDownMenu(["item 1", "item 2"], "item 1", test_rect)
+        }
+    }
+    return questionnaire
+
+
+# class TestUISection:
+#     # TODO: Add tests for UISections
+#
+#     def test_creation(self, _init_pygame, default_ui_manager, _display_surface_return_none):
+#         form = UIForm(pygame.Rect(100, 100, 200, 200), questionnaire=get_questionnaire(), manager=default_ui_manager)
+#         UISection(pygame.Rect(100, 100, 200, 200), form, "section", questionnaire=get_questionnaire(), manager=default_ui_manager)
+
+
+class TestUIForm:
+
+    # TODO: Add tests testing various inputs
+    # TODO: Add tests for validate_questionnaire
+    # TODO: Add tests for theming
+
+    def test_creation(self, _init_pygame, default_ui_manager, _display_surface_return_none):
+        UIForm(pygame.Rect(100, 100, 200, 200), questionnaire=get_questionnaire(), manager=default_ui_manager)
+
+    def test_get_container(self, _init_pygame, default_ui_manager: IUIManagerInterface,
+                           _display_surface_return_none):
+        container = UIForm(pygame.Rect(100, 100, 200, 200), questionnaire=get_questionnaire(),
+                           manager=default_ui_manager)
+        assert container.get_container() == container.scrollable_container
+
+    def test_add_element(self, _init_pygame, default_ui_manager: IUIManagerInterface,
+                         _display_surface_return_none):
+        container = UIForm(pygame.Rect(100, 100, 200, 200), questionnaire=get_questionnaire(),
+                           manager=default_ui_manager)
+        assert len(container.get_container().elements) == 16
+        assert len(container.parsed_questionnaire["Section Test:"].elements) == 2
+        assert len(container.parsed_questionnaire["Section Test:"].section_container.elements) == 7
+
+        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.get_container().add_element(button)
+        assert len(container.get_container().elements) == 17
+
+        button = UIButton(relative_rect=pygame.Rect(0, 0, 50, 250), text="",
+                          manager=default_ui_manager)
+        default_ui_manager.get_root_container().remove_element(button)
+        container.get_container().add_element(button)
+        container.scrollable_container.update(0.4)
+        container.update(0.4)
+        assert len(container.get_container().elements) == 18
+
+    def test_remove_element(self, _init_pygame, default_ui_manager: IUIManagerInterface,
+                            _display_surface_return_none):
+        container = UIForm(pygame.Rect(100, 100, 200, 200), questionnaire=get_questionnaire(),
+                           manager=default_ui_manager)
+        assert len(container.get_container().elements) == 16
+
+        button = UIButton(relative_rect=pygame.Rect(0, 0, 50, 50), text="",
+                          manager=default_ui_manager,
+                          container=container)
+
+        container.get_container().remove_element(button)
+        assert len(container.get_container().elements) == 16
+
+    def test_set_position(self, _init_pygame, default_ui_manager,
+                          _display_surface_return_none):
+        container = UIForm(pygame.Rect(100, 100, 200, 200), questionnaire=get_questionnaire(),
+                           manager=default_ui_manager)
+
+        container.set_position((50, 50))
+
+        assert container.rect.topleft == (50, 50)
+        assert container._root_container.rect.size == (194, 194)
+
+    def test_set_relative_position(self, _init_pygame, default_ui_manager,
+                                   _display_surface_return_none):
+        container = UIForm(pygame.Rect(100, 100, 200, 200), questionnaire=get_questionnaire(),
+                           manager=default_ui_manager)
+        container_2 = UIForm(pygame.Rect(50, 50, 50, 50),
+                             manager=default_ui_manager,
+                             container=container)
+
+        container_2.set_relative_position((25, 25))
+
+        assert container_2.rect.topleft == (128, 128)
+
+    def test_set_dimensions(self, _init_pygame, default_ui_manager,
+                            _display_surface_return_none):
+        container = UIForm(pygame.Rect(100, 100, 200, 200), questionnaire=get_questionnaire(),
+                           manager=default_ui_manager)
+        # TODO: Find out why it crashes when set to 50 or lesser in x axis or 5 or lesser in y axis
+        # TODO: Find out why it crashes when placing a UIForm inside a UIForm
+        container.set_dimensions((100, 100))
+        assert container.rect.size == (100, 100)
+
+    def test_disable(self, _init_pygame: None, default_ui_manager: UIManager,
+                     _display_surface_return_none: None):
+        container = UIForm(pygame.Rect(100, 100, 200, 200), questionnaire=get_questionnaire(),
+                           manager=default_ui_manager)
+        button_1 = UIButton(relative_rect=pygame.Rect(10, 10, 150, 30),
+                            text="Test Button",
+                            tool_tip_text="This is a test of the button's tool tip functionality.",
+                            manager=default_ui_manager,
+                            container=container)
+
+        button_2 = UIButton(relative_rect=pygame.Rect(10, 50, 150, 30),
+                            text="Test Button 2",
+                            manager=default_ui_manager,
+                            container=container)
+
+        element = container.parsed_questionnaire["Character Test:"].element
+
+        container.disable()
+
+        assert container.is_enabled is False
+        assert button_1.is_enabled is False
+        assert button_2.is_enabled is False
+        assert element.is_enabled is False
+
+        # process a mouse button down event
+        button_1.process_event(
+            pygame.event.Event(pygame.MOUSEBUTTONDOWN, {'button': 1, 'pos': button_1.rect.center}))
+
+        # process a mouse button up event
+        button_1.process_event(
+            pygame.event.Event(pygame.MOUSEBUTTONUP, {'button': 1, 'pos': button_1.rect.center}))
+
+        button_1.update(0.01)
+
+        assert button_1.check_pressed() is False
+
+        # process input
+
+        default_ui_manager.mouse_position = element.rect.center
+        default_ui_manager.process_events(
+            pygame.event.Event(pygame.MOUSEBUTTONDOWN, {"button": pygame.BUTTON_LEFT, "pos": element.rect.center})
+        )
+        default_ui_manager.process_events(
+            pygame.event.Event(pygame.MOUSEBUTTONUP, {"button": pygame.BUTTON_LEFT, "pos": element.rect.center})
+        )
+        element.process_event(pygame.event.Event(pygame.TEXTINPUT, {"text": "abc"}))
+        assert container.get_current_values()["Character Test:"] == ''
+
+    def test_enable(self, _init_pygame: None, default_ui_manager: UIManager,
+                    _display_surface_return_none: None):
+        container = UIForm(pygame.Rect(100, 100, 200, 200), questionnaire=get_questionnaire(),
+                           manager=default_ui_manager)
+        button_1 = UIButton(relative_rect=pygame.Rect(10, 10, 150, 30),
+                            text="Test Button",
+                            tool_tip_text="This is a test of the button's tool tip functionality.",
+                            manager=default_ui_manager,
+                            container=container)
+
+        button_2 = UIButton(relative_rect=pygame.Rect(10, 50, 150, 30),
+                            text="Test Button 2",
+                            manager=default_ui_manager,
+                            container=container)
+
+        element = container.parsed_questionnaire["Character Test:"].element
+
+        container.disable()
+        container.enable()
+
+        assert container.is_enabled is True
+        assert button_1.is_enabled is True
+        assert button_2.is_enabled is True
+        assert element.is_enabled is True
+
+        # process a mouse button down event
+        button_1.process_event(
+            pygame.event.Event(pygame.MOUSEBUTTONDOWN, {'button': 1, 'pos': button_1.rect.center}))
+
+        # process a mouse button up event
+        button_1.process_event(
+            pygame.event.Event(pygame.MOUSEBUTTONUP, {'button': 1, 'pos': button_1.rect.center}))
+
+        button_1.update(0.01)
+
+        assert button_1.check_pressed() is True
+
+        # process input
+
+        default_ui_manager.mouse_position = element.rect.center
+        default_ui_manager.process_events(
+            pygame.event.Event(pygame.MOUSEBUTTONDOWN, {"button": pygame.BUTTON_LEFT, "pos": element.rect.center})
+        )
+        default_ui_manager.process_events(
+            pygame.event.Event(pygame.MOUSEBUTTONUP, {"button": pygame.BUTTON_LEFT, "pos": element.rect.center})
+        )
+        element.process_event(pygame.event.Event(pygame.TEXTINPUT, {"text": "abc"}))
+        assert container.get_current_values()["Character Test:"] == 'a'
+
+    def test_show(self, _init_pygame, default_ui_manager,
+                  _display_surface_return_none):
+        container = UIForm(relative_rect=pygame.Rect(100, 100, 400, 400),
+                           manager=default_ui_manager, visible=0)
+
+        assert container.visible == 0
+
+        assert container._root_container.visible == 0
+        assert container._view_container.visible == 0
+        assert container.vert_scroll_bar.visible == 0
+        assert container.scrollable_container.visible == 0
+
+        container.show()
+
+        assert container.visible == 1
+
+        assert container._root_container.visible == 1
+        assert container._view_container.visible == 1
+        assert container.vert_scroll_bar.visible == 1
+        assert container.scrollable_container.visible == 1
+
+    def test_hide(self, _init_pygame, default_ui_manager,
+                  _display_surface_return_none):
+        container = UIForm(relative_rect=pygame.Rect(100, 100, 400, 400),
+                           manager=default_ui_manager)
+
+        assert container.visible == 1
+
+        assert container._root_container.visible == 1
+        assert container._view_container.visible == 1
+        assert container.vert_scroll_bar.visible == 1
+        assert container.scrollable_container.visible == 1
+
+        container.hide()
+
+        assert container.visible == 0
+
+        assert container._root_container.visible == 0
+        assert container._view_container.visible == 0
+        assert container.vert_scroll_bar.visible == 0
+        assert container.scrollable_container.visible == 0
+
+    def test_show_hide_rendering(self, _init_pygame, default_ui_manager, _display_surface_return_none):
+        resolution = (400, 400)
+        empty_surface = pygame.Surface(resolution)
+        empty_surface.fill(pygame.Color(0, 0, 0))
+
+        surface = empty_surface.copy()
+        manager = UIManager(resolution)
+
+        container = UIForm(relative_rect=pygame.Rect(100, 100, 200, 100),
+                           manager=manager,
+                           visible=0)
+        manager.update(0.01)
+        manager.draw_ui(surface)
+        assert compare_surfaces(empty_surface, surface)
+
+        surface.fill(pygame.Color(0, 0, 0))
+        container.show()
+        manager.update(0.01)
+        manager.draw_ui(surface)
+        assert not compare_surfaces(empty_surface, surface)
+
+        surface.fill(pygame.Color(0, 0, 0))
+        container.hide()
+        manager.update(0.01)
+        manager.draw_ui(surface)
+        assert compare_surfaces(empty_surface, surface)
+
+    def test_get_current_values(self, _init_pygame, default_ui_manager: IUIManagerInterface,
+                                _display_surface_return_none):
+        questionnaire = get_questionnaire()
+        container = UIForm(pygame.Rect(100, 100, 200, 200), questionnaire=questionnaire,
+                           manager=default_ui_manager)
+
+        assert container.get_current_values() == {'Character Test:': '', 'Short Text Test:': '', 'Long Text Test:': '',
+                                                  'Integer Test:': 0, 'Decimal Test:': 0.0, 'Password Test:': '',
+                                                  'Boolean Test:': True,
+                                                  'Section Test:': {
+                                                      'UITextEntryLine Test': '',
+                                                      'UITextEntryBox Test': '',
+                                                      'UISelectionList Test': None,
+                                                      'UISelectionList Multiselect Test': [],
+                                                      'UIDropDownMenu Test': 'item 1'
+                                                  }
+                                                  }
+
+        container.kill()
+
+        # Character Test
+
+        container = UIForm(pygame.Rect(100, 100, 200, 200), questionnaire=questionnaire,
+                           manager=default_ui_manager)
+        element = container.parsed_questionnaire["Character Test:"].element
+
+        default_ui_manager.mouse_position = element.rect.center
+        default_ui_manager.process_events(
+            pygame.event.Event(pygame.MOUSEBUTTONDOWN, {"button": pygame.BUTTON_LEFT, "pos": element.rect.center})
+        )
+        default_ui_manager.process_events(
+            pygame.event.Event(pygame.MOUSEBUTTONUP, {"button": pygame.BUTTON_LEFT, "pos": element.rect.center})
+        )
+        element.process_event(pygame.event.Event(pygame.TEXTINPUT, {"text": "abc"}))
+        assert container.get_current_values()["Character Test:"] == 'a'
+
+        container.kill()
+        questionnaire.pop("Character Test:")
+
+        # Short Text Test
+
+        container = UIForm(pygame.Rect(100, 100, 200, 200), questionnaire=questionnaire,
+                           manager=default_ui_manager)
+        element = container.parsed_questionnaire["Short Text Test:"].element
+
+        default_ui_manager.mouse_position = element.rect.center
+        default_ui_manager.process_events(
+            pygame.event.Event(pygame.MOUSEBUTTONDOWN, {"button": pygame.BUTTON_LEFT, "pos": element.rect.center})
+        )
+        default_ui_manager.process_events(
+            pygame.event.Event(pygame.MOUSEBUTTONUP, {"button": pygame.BUTTON_LEFT, "pos": element.rect.center})
+        )
+        element.process_event(pygame.event.Event(pygame.TEXTINPUT, {"text": "abc"}))
+        assert container.get_current_values()["Short Text Test:"] == 'abc'
+
+        container.kill()
+        questionnaire.pop("Short Text Test:")
+
+        # Long Text Test
+
+        container = UIForm(pygame.Rect(100, 100, 200, 200), questionnaire=questionnaire,
+                           manager=default_ui_manager)
+        element = container.parsed_questionnaire["Long Text Test:"].element
+
+        default_ui_manager.mouse_position = element.rect.center
+        default_ui_manager.process_events(
+            pygame.event.Event(pygame.MOUSEBUTTONDOWN, {"button": pygame.BUTTON_LEFT, "pos": element.rect.center})
+        )
+        default_ui_manager.process_events(
+            pygame.event.Event(pygame.MOUSEBUTTONUP, {"button": pygame.BUTTON_LEFT, "pos": element.rect.center})
+        )
+
+        element.process_event(pygame.event.Event(pygame.TEXTINPUT, {"text": "abc"}))
+        assert container.get_current_values()["Long Text Test:"] == 'abc'
+
+        container.kill()
+        questionnaire.pop("Long Text Test:")
+
+        # Integer Test
+
+        container = UIForm(pygame.Rect(100, 100, 200, 200), questionnaire=questionnaire,
+                           manager=default_ui_manager)
+        element = container.parsed_questionnaire["Integer Test:"].element
+
+        default_ui_manager.mouse_position = element.rect.center
+        default_ui_manager.process_events(
+            pygame.event.Event(pygame.MOUSEBUTTONDOWN, {"button": pygame.BUTTON_LEFT, "pos": element.rect.center})
+        )
+        default_ui_manager.process_events(
+            pygame.event.Event(pygame.MOUSEBUTTONUP, {"button": pygame.BUTTON_LEFT, "pos": element.rect.center})
+        )
+        element.process_event(pygame.event.Event(pygame.TEXTINPUT, {"text": "a1.b2"}))
+        assert container.get_current_values()["Integer Test:"] == 12
+
+        container.kill()
+        questionnaire.pop("Integer Test:")
+
+        # Decimal Test
+
+        container = UIForm(pygame.Rect(100, 100, 200, 200), questionnaire=questionnaire,
+                           manager=default_ui_manager)
+
+        element = container.parsed_questionnaire["Decimal Test:"].element
+
+        default_ui_manager.mouse_position = element.rect.center
+        default_ui_manager.process_events(
+            pygame.event.Event(pygame.MOUSEBUTTONDOWN, {"button": pygame.BUTTON_LEFT, "pos": element.rect.center})
+        )
+        default_ui_manager.process_events(
+            pygame.event.Event(pygame.MOUSEBUTTONUP, {"button": pygame.BUTTON_LEFT, "pos": element.rect.center})
+        )
+        element.process_event(pygame.event.Event(pygame.TEXTINPUT, {"text": "a1.b2"}))
+        assert container.get_current_values()["Decimal Test:"] == 1.2
+        element.process_event(pygame.event.Event(pygame.TEXTINPUT, {"text": "a1.b2.c3"}))
+        assert container.get_current_values()["Decimal Test:"] == 0.0  # Invalid input results in 0.0
+
+        container.kill()
+        questionnaire.pop("Decimal Test:")
+
+        # Password Test
+
+        container = UIForm(pygame.Rect(100, 100, 200, 200), questionnaire=questionnaire,
+                           manager=default_ui_manager)
+
+        element = container.parsed_questionnaire["Password Test:"].element
+
+        default_ui_manager.mouse_position = element.rect.center
+        default_ui_manager.process_events(
+            pygame.event.Event(pygame.MOUSEBUTTONDOWN, {"button": pygame.BUTTON_LEFT, "pos": element.rect.center})
+        )
+        default_ui_manager.process_events(
+            pygame.event.Event(pygame.MOUSEBUTTONUP, {"button": pygame.BUTTON_LEFT, "pos": element.rect.center})
+        )
+        element.process_event(pygame.event.Event(pygame.TEXTINPUT, {"text": "abc"}))
+        assert container.get_current_values()["Password Test:"] == 'abc'
+
+        container.kill()
+        questionnaire.pop("Password Test:")
+
+        # Boolean Test
+
+        container = UIForm(pygame.Rect(100, 100, 200, 200), questionnaire=questionnaire,
+                           manager=default_ui_manager)
+
+        element = container.parsed_questionnaire["Boolean Test:"].element
+
+        # Simulate Opening a drop-down
+        button = element.current_state.open_button
+        default_ui_manager.mouse_position = element.rect.center
+        default_ui_manager.process_events(
+            pygame.event.Event(pygame_gui.UI_BUTTON_PRESSED, {"mouse_button": pygame.BUTTON_LEFT, 'ui_element': button,
+                                                              'ui_object_id': button.most_specific_combined_id})
+        )
+
+        element.update(0.1)
+
+        # Simulate selecting an option
+        element = element.current_state.options_selection_list.item_list[-1]["button_element"]
+        default_ui_manager.process_events(
+            pygame.event.Event(pygame.MOUSEBUTTONDOWN, {"button": pygame.BUTTON_LEFT, "pos": element.rect.center})
+        )
+        default_ui_manager.process_events(
+            pygame.event.Event(pygame.MOUSEBUTTONUP, {"button": pygame.BUTTON_LEFT, "pos": element.rect.center})
+        )
+        assert container.get_current_values()["Boolean Test:"] is True
+
+        container.kill()
+        questionnaire.pop("Boolean Test:")
+
+    def test_rebuild_from_theme_data_non_default(self, _init_pygame,
+                                                 _display_surface_return_none):
+        manager = UIManager((800, 600), os.path.join("tests", "data",
+                                                     "themes",
+                                                     "ui_2d_slider_non_default.json"))
+
+    @pytest.mark.filterwarnings("ignore:Invalid value")
+    @pytest.mark.filterwarnings("ignore:Colour hex code")
+    @pytest.mark.filterwarnings("ignore:Invalid Theme Colour")
+    def test_rebuild_from_theme_data_bad_values(self, _init_pygame,
+                                                _display_surface_return_none):
+        manager = UIManager((800, 600), os.path.join("tests",
+                                                     "data",
+                                                     "themes",
+                                                     "ui_2d_slider_bad_values.json"))
+
+    def test_kill(self, _init_pygame, default_ui_manager,
+                  _display_surface_return_none):
+        container = UIForm(pygame.Rect(100, 100, 200, 200), questionnaire=get_questionnaire(),
+                           manager=default_ui_manager)
+        container_2 = UIForm(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_check_hover_when_not_able_to_hover(self, _init_pygame, default_ui_manager,
+                                                _display_surface_return_none):
+        container = UIForm(pygame.Rect(100, 100, 200, 200), questionnaire=get_questionnaire(),
+                           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_type_checker(self, _init_pygame, default_ui_manager,
+                          _display_surface_return_none):
+        # General
+
+        value = UIForm.type_checker("character")
+        assert isinstance(value, tuple)
+        assert isinstance(value[0], str)
+        assert isinstance(value[1], dict)
+
+        # White Space
+        w = " \n\t\v\r\f"  # stands for white_spaces
+        assert UIForm.type_checker(f"{w}short_text{w}({w}' a' \t\f'b' \t\f\"c\"{w},{w}required{w}={w}True{w}){w}") == ("short_text", {"default": " abc", "required": True})
+
+        # Syntax + Character
+
+        assert UIForm.type_checker("character") == ("character", {})
+        assert UIForm.type_checker("character('a')") == ("character", {"default": "a"})
+        assert UIForm.type_checker("character(default='a')") == ("character", {"default": "a"})
+        assert UIForm.type_checker("character('a',True)") == ("character", {"default": "a", "required": True})
+        assert UIForm.type_checker("character('a',False)") == ("character", {"default": "a", "required": False})
+        assert UIForm.type_checker("character('a',required=True)") == ("character", {"default": "a", "required": True})
+        assert UIForm.type_checker("character(default='a',required=False)") == ("character", {"default": "a", "required": False})
+
+        # Short Text
+        assert UIForm.type_checker("short_text('abc', required=True)") == ("short_text", {"default": "abc", "required": True})
+
+        # Long Text
+        assert UIForm.type_checker("long_text('abc', required=True)") == ("long_text", {"default": "abc", "required": True})
+
+        # Integer Text
+        assert UIForm.type_checker("integer(1,required=True)") == ("integer", {"default": 1, "required": True})
+
+        # Decimal Text
+        assert UIForm.type_checker("decimal(1,required=True)") == ("decimal", {"default": 1, "required": True})
+        assert UIForm.type_checker("decimal(1.0,required=False)") == ("decimal", {"default": 1.0, "required": False})
+
+        # Password Text
+        assert UIForm.type_checker("password('abc', True)") == ("password", {"default": "abc", "required": True})
+
+        # Boolean Text
+        assert UIForm.type_checker("boolean(True, True)") == ("boolean", {"default": True, "required": True})
+        assert UIForm.type_checker("boolean(False, False)") == ("boolean", {"default": False, "required": False})
+
+    def test_type_checker_bad_values(self, _init_pygame, default_ui_manager,
+                                     _display_surface_return_none):
+        # General
+
+        with pytest.raises(ValueError, match="Question type 'unknown' is not supported"):
+            UIForm.type_checker("unknown")
+
+        with pytest.raises(SyntaxError, match="Parenthesis used without passing parameters for question"):
+            UIForm.type_checker("character()")
+
+        with pytest.raises(SyntaxError, match="Found whitespace between keyword or value"):
+            UIForm.type_checker("character(de fault='a')")
+
+        with pytest.raises(SyntaxError, match="Found whitespace between keyword or value"):
+            UIForm.type_checker("character(a b)")
+
+        with pytest.raises(SyntaxError, match="Found '=' at the beginning of arguments"):
+            UIForm.type_checker("character(=)")
+
+        with pytest.raises(SyntaxError, match="Found keyword wrapped in quotes"):
+            UIForm.type_checker("character('default'='a')")
+
+        with pytest.raises(SyntaxError, match="No keyword passed for argument before '=' symbol"):
+            UIForm.type_checker("character(default='a',=)")
+
+        with pytest.raises(SyntaxError, match="Found 2 consecutive '=' symbols"):
+            UIForm.type_checker("character(default==)")
+
+        with pytest.raises(SyntaxError, match="Found comma at the beginning of arguments"):
+            UIForm.type_checker("character(,)")
+
+        with pytest.raises(SyntaxError, match="Found 2 consecutive commas"):
+            UIForm.type_checker("character(default='a',,)")
+
+        with pytest.raises(SyntaxError, match="No value passed for keyword argument 'default'"):
+            UIForm.type_checker("character(default=,)")
+
+        with pytest.raises(NameError, match="'a' is not defined. Perhaps quotes were missed"):
+            UIForm.type_checker("character(default=a)")
+
+        with pytest.raises(NameError, match="'a' is not defined. Perhaps quotes were missed"):
+            UIForm.type_checker("character(a)")
+
+        with pytest.raises(SyntaxError, match="Found unmatched '"):
+            UIForm.type_checker("character(')")
+
+        with pytest.raises(SyntaxError, match="Found unmatched \""):
+            UIForm.type_checker("character(\")")
+
+        with pytest.raises(ValueError, match="Question of type 'character' take at most 2 arguments"
+                                             " but 3 arguments were passed"):
+            UIForm.type_checker("character('a','b','c')")
+
+        with pytest.raises(SyntaxError, match="Found positional argument after keyword argument"):
+            UIForm.type_checker("character(default='a', 'b')")
+
+        with pytest.raises(ValueError, match="Unknown parameter 'unknown' for question type 'character'"):
+            UIForm.type_checker("character(unknown = 'a')")
+
+        with pytest.raises(SyntaxError, match="Got multiple values for parameter 'default'"):
+            UIForm.type_checker("character('a', default='a')")
+
+        # Character
+
+        with pytest.raises(ValueError, match="Invalid value ''ab'' for argument 'default'"):
+            UIForm.type_checker("character(default='ab')")
+
+        with pytest.raises(ValueError, match="Invalid value ''ab'' for argument 'default'"):
+            UIForm.type_checker("character('ab')")
+
+        # Integer
+
+        with pytest.raises(ValueError, match="Invalid value ''a'' for argument 'default'"):
+            UIForm.type_checker("integer('a')")
+
+        with pytest.raises(ValueError, match="Invalid value '2.0' for argument 'default'"):
+            UIForm.type_checker("integer(2.0)")
+
+        with pytest.raises(ValueError, match="Invalid value ''2'' for argument 'default'"):
+            UIForm.type_checker("integer('2')")
+
+        # Decimal
+
+        with pytest.raises(ValueError, match="Invalid value ''a'' for argument 'default'"):
+            UIForm.type_checker("decimal('a')")
+
+        with pytest.raises(ValueError, match="Invalid value ''2.0'' for argument 'default'"):
+            UIForm.type_checker("decimal('2.0')")
+
+        # Boolean
+
+        with pytest.raises(ValueError, match="Invalid value ''a'' for argument 'default'"):
+            UIForm.type_checker("boolean('a')")
+
+        with pytest.raises(ValueError, match="Invalid value ''True'' for argument 'default'"):
+            UIForm.type_checker("boolean(default='True')")
+
+        with pytest.raises(ValueError, match="Invalid value ''False'' for argument 'required'"):
+            UIForm.type_checker("boolean(required='False')")
+
+    def test_validate_questionnaire(self, _init_pygame, default_ui_manager,
+                                    _display_surface_return_none):
+        container = UIForm(pygame.Rect(100, 100, 200, 200), questionnaire=get_questionnaire(),
+                           manager=default_ui_manager)
+
+        container.kill()
+
+        assert not container.alive()
+
+    def test_validate_questionnaire_bad_values(self, _init_pygame, default_ui_manager,
+                                               _display_surface_return_none):
+        container = UIForm(pygame.Rect(100, 100, 200, 200), questionnaire=get_questionnaire(),
+                           manager=default_ui_manager)
+
+        container.kill()
+
+        assert not container.alive()
+
+
+if __name__ == '__main__':
+    pytest.console_main()
diff --git a/tests/test_elements/test_ui_scrolling_container.py b/tests/test_elements/test_ui_scrolling_container.py
index 1fe3dca..9704b23 100644
--- a/tests/test_elements/test_ui_scrolling_container.py
+++ b/tests/test_elements/test_ui_scrolling_container.py
@@ -385,7 +385,11 @@ class TestUIScrollingContainer:
         container = UIScrollingContainer(relative_rect=pygame.Rect(100, 100, 200, 100),
                                          manager=manager,
                                          visible=0)
-        container.set_scrollable_area_dimensions((600, 600))
+        button_1 = UIButton(relative_rect=pygame.Rect(300, 100, 150, 30),
+                            text="Test Button",
+                            tool_tip_text="This is a test of the button's tool tip functionality.",
+                            manager=default_ui_manager,
+                            container=container)
         manager.update(0.01)
         manager.draw_ui(surface)
         assert compare_surfaces(empty_surface, surface)