Repository: MyreMylar/pygame_gui (python)

Issue:
pygame_gui.windows.UIFileDialog changing object_id has strange behavior
**Describe the bug**
With class pygame_gui.windows.UIFileDialog, setting object_id to anything other than the default "#file_dialog" results in the four little buttons for home, unknown, parent and refresh to not render correctly. They show up as solid grey boxes with no symbols in them.

**To Reproduce**
 test_file =  UIFileDialog(rect=pygame.Rect(100, 100, 600, 500),
                     manager=self.ui_manager,
                     window_title="save_pizza",
                     object_id="#save_pizza")
test_file.show()

**Expected behaviour**
The four icons to show their symbols correctly.

**Screenshots**
![image](https://user-images.githubusercontent.com/25121019/234930378-59762d1f-577c-4c70-a96d-db3c851df783.png)
![image](https://user-images.githubusercontent.com/25121019/234930650-24a5da5f-b1b1-4f73-9fba-430b4ba8861d.png)



Pull request description:
Add 'base_id' capability to allow windows to have two element ids
A window can now be both a 'window' and e.g. a 'file_dialog'.

fixes #451

Patch:
diff --git a/docs/source/theme_reference/theme_colour_picker_dialog.rst b/docs/source/theme_reference/theme_colour_picker_dialog.rst
index 80e6490..1ce87be 100644
--- a/docs/source/theme_reference/theme_colour_picker_dialog.rst
+++ b/docs/source/theme_reference/theme_colour_picker_dialog.rst
@@ -3,7 +3,8 @@
 UIColourPickerDialog Theming Parameters
 =======================================
 
-:class:`UIColourPickerDialog <pygame_gui.windows.UIColourPickerDialog>` is a UIWindow with the object id of '#colour_picker_dialog'.
+:class:`UIColourPickerDialog <pygame_gui.windows.UIColourPickerDialog>` is a UIWindow with the element id of
+'colour_picker_dialog' and the default object id of '#colour_picker_dialog'.
 
 .. figure:: ../_static/colour_picker_static_image.png
 
diff --git a/docs/source/theme_reference/theme_confirmation_dialog.rst b/docs/source/theme_reference/theme_confirmation_dialog.rst
index ab1ade7..b3b6dcd 100644
--- a/docs/source/theme_reference/theme_confirmation_dialog.rst
+++ b/docs/source/theme_reference/theme_confirmation_dialog.rst
@@ -3,7 +3,8 @@
 UIConfirmationDialog Theming Parameters
 =======================================
 
-:class:`UIConfirmationDialog <pygame_gui.windows.UIConfirmationDialog>` is a UIWindow with the object id of '#confirmation_dialog'.
+:class:`UIConfirmationDialog <pygame_gui.windows.UIConfirmationDialog>` is a UIWindow with the element id of
+'confirmation_dialog' and a default object id of '#confirmation_dialog'.
 
 .. figure:: ../_static/confirmation_dialog_static_image.png
 
@@ -23,11 +24,11 @@ As well as the sub-elements of the UIWindow (title bar and close button) which y
 
 UIButtons:
 
- - '#confirmation_dialog.#confirm_button'
- - '#confirmation_dialog.#cancel_button'
+ - 'confirmation_dialog.#confirm_button'
+ - 'confirmation_dialog.#cancel_button'
 
 UITextBox:
 
- - '#confirmation_dialog.text_box'
+ - 'confirmation_dialog.text_box'
 
 You can find out more about theming buttons here: :ref:`theme-button` and text boxes here: :ref:`theme-text-box`.
\ No newline at end of file
diff --git a/docs/source/theme_reference/theme_file_dialog.rst b/docs/source/theme_reference/theme_file_dialog.rst
index b966b84..5273c69 100644
--- a/docs/source/theme_reference/theme_file_dialog.rst
+++ b/docs/source/theme_reference/theme_file_dialog.rst
@@ -3,7 +3,8 @@
 UIFileDialog Theming Parameters
 ===============================
 
-:class:`UIFileDialog <pygame_gui.windows.UIFileDialog>` is a UIWindow with the object id of '#file_dialog'.
+:class:`UIFileDialog <pygame_gui.windows.UIFileDialog>` is a UIWindow with the element id of 'file_dialog' and a default
+object id of '#file_dialog'.
 
 
 .. figure:: ../_static/file_dialog_static_image.png
@@ -25,20 +26,20 @@ As well as the sub-elements of the UIWindow (title bar and close button) which y
 
 UIButtons:
 
- - '#file_dialog.#ok_button'
- - '#file_dialog.#cancel_button'
- - '#file_dialog.#home_icon_button'
- - '#file_dialog.#delete_icon_button'
- - '#file_dialog.#parent_icon_button'
- - '#file_dialog.#refresh_icon_button'
+ - 'file_dialog.#ok_button'
+ - 'file_dialog.#cancel_button'
+ - 'file_dialog.#home_icon_button'
+ - 'file_dialog.#delete_icon_button'
+ - 'file_dialog.#parent_icon_button'
+ - 'file_dialog.#refresh_icon_button'
 
 UITextEntryLine:
 
- - '#file_dialog.#file_path_text_line'
+ - 'file_dialog.#file_path_text_line'
 
 UISelectionList:
 
- - '#file_dialog.#file_display_list'
+ - 'file_dialog.#file_display_list'
 
 You can find out more about theming buttons here: :ref:`theme-button`, text entry lines here: :ref:`theme-text-entry-line`
 and selection lists here: :ref:`theme-selection-list`.
diff --git a/docs/source/theme_reference/theme_message_window.rst b/docs/source/theme_reference/theme_message_window.rst
index 044d926..ce5c0ca 100644
--- a/docs/source/theme_reference/theme_message_window.rst
+++ b/docs/source/theme_reference/theme_message_window.rst
@@ -3,7 +3,8 @@
 UIMessageWindow Theming Parameters
 =======================================
 
-:class:`UIMessageWindow <pygame_gui.windows.UIMessageWindow>` is a UIWindow with the object id of '#message_window'.
+:class:`UIMessageWindow <pygame_gui.windows.UIMessageWindow>` is a UIWindow with the element id of 'message_window' and
+a default object id of '#message_window'.
 
 .. figure:: ../_static/message_window_static_image.png
 
@@ -23,10 +24,10 @@ As well as the sub-elements of the UIWindow (title bar and close button) which y
 
 UIButtons:
 
- - '#message_window.#dismiss_button'
+ - 'message_window.#dismiss_button'
 
 UITextBox:
 
- - '#message_window.text_box'
+ - 'message_window.text_box'
 
 You can find out more about theming buttons here: :ref:`theme-button` and text boxes here: :ref:`theme-text-box`.
diff --git a/pygame_gui/core/interfaces/appearance_theme_interface.py b/pygame_gui/core/interfaces/appearance_theme_interface.py
index 38f9938..bf74243 100644
--- a/pygame_gui/core/interfaces/appearance_theme_interface.py
+++ b/pygame_gui/core/interfaces/appearance_theme_interface.py
@@ -55,12 +55,14 @@ class IUIAppearanceThemeInterface(metaclass=ABCMeta):
         """
 
     @abstractmethod
-    def build_all_combined_ids(self, element_ids: Union[None, List[str]],
+    def build_all_combined_ids(self, element_base_ids: Union[None, List[Union[str, None]]],
+                               element_ids: Union[None, List[str]],
                                class_ids: Union[None, List[Union[str, None]]],
                                object_ids: Union[None, List[Union[str, None]]]) -> List[str]:
         """
         Construct a list of combined element ids from the element's various accumulated ids.
 
+        :param element_base_ids: when an element is also another element (e.g. a file dialog is also a window)
         :param element_ids: All the ids of elements this element is contained within.
         :param class_ids: All the ids of 'classes' that this element is contained within.
         :param object_ids: All the ids of objects this element is contained within.
diff --git a/pygame_gui/core/interfaces/element_interface.py b/pygame_gui/core/interfaces/element_interface.py
index 7547703..9655957 100644
--- a/pygame_gui/core/interfaces/element_interface.py
+++ b/pygame_gui/core/interfaces/element_interface.py
@@ -29,6 +29,14 @@ class IUIElementInterface(metaclass=ABCMeta):
 
         """
 
+    @abstractmethod
+    def get_element_base_ids(self) -> List[str]:
+        """
+        A list of all the element base IDs in this element's theming/event hierarchy.
+
+        :return: a list of strings, one for each element in the hierarchy.
+        """
+
     @abstractmethod
     def get_element_ids(self) -> List[str]:
         """
diff --git a/pygame_gui/core/ui_appearance_theme.py b/pygame_gui/core/ui_appearance_theme.py
index 97953ae..02dac31 100644
--- a/pygame_gui/core/ui_appearance_theme.py
+++ b/pygame_gui/core/ui_appearance_theme.py
@@ -373,6 +373,7 @@ class UIAppearanceTheme(IUIAppearanceThemeInterface):
                                                                     shape_corner_radius)
 
     def _get_next_id_node(self, current_node: Union[Dict[str, Union[str, Dict]], None],
+                          element_base_ids: List[Union[str, None]],
                           element_ids: List[str],
                           class_ids: List[Union[str, None]],
                           object_ids: List[Union[str, None]],
@@ -405,19 +406,27 @@ class UIAppearanceTheme(IUIAppearanceThemeInterface):
                     and index < len(object_ids)
                     and object_ids[index] is not None):
                 next_node = {'id': object_ids[index], 'parent': current_node}
-                self._get_next_id_node(next_node, element_ids, class_ids, object_ids,
+                self._get_next_id_node(next_node, element_base_ids, element_ids, class_ids, object_ids,
                                        index + 1, tree_size, combined_ids)
             # then any class IDs...
             if (class_ids is not None
                     and index < len(class_ids)
                     and class_ids[index] is not None):
                 next_node = {'id': class_ids[index], 'parent': current_node}
-                self._get_next_id_node(next_node, element_ids, class_ids, object_ids,
+                self._get_next_id_node(next_node, element_base_ids, element_ids, class_ids, object_ids,
                                        index + 1, tree_size, combined_ids)
             # Finally add the required element IDs.
             next_node_2 = {'id': element_ids[index], 'parent': current_node}
-            self._get_next_id_node(next_node_2, element_ids, class_ids, object_ids, index + 1,
+            self._get_next_id_node(next_node_2, element_base_ids, element_ids, class_ids, object_ids, index + 1,
                                    tree_size, combined_ids)
+
+            # then any element base IDs...
+            if (element_base_ids is not None
+                    and index < len(element_base_ids)
+                    and element_base_ids[index] is not None):
+                next_node = {'id': element_base_ids[index], 'parent': current_node}
+                self._get_next_id_node(next_node, element_base_ids, element_ids, class_ids, object_ids,
+                                       index + 1, tree_size, combined_ids)
         else:
             # unwind
             gathered_ids = []
@@ -432,25 +441,28 @@ class UIAppearanceTheme(IUIAppearanceThemeInterface):
                 combined_id += gathered_ids[gathered_index]
             combined_ids.append(combined_id)
 
-    def build_all_combined_ids(self, element_ids: Union[None, List[str]],
+    def build_all_combined_ids(self, element_base_ids: Union[None, List[Union[str, None]]],
+                               element_ids: Union[None, List[str]],
                                class_ids: Union[None, List[Union[str, None]]],
                                object_ids: Union[None, List[Union[str, None]]]) -> List[str]:
         """
         Construct a list of combined element ids from the element's various accumulated ids.
 
+        :param element_base_ids: when an element is also another element (e.g. a file dialog is also a window)
         :param element_ids: All the ids of elements this element is contained within.
         :param class_ids: All the ids of 'classes' that this element is contained within.
         :param object_ids: All the ids of objects this element is contained within.
 
         :return: A list of IDs that reference this element in order of decreasing specificity.
         """
-        combined_id = str(element_ids).join(str(class_ids)).join(str(object_ids))
+        combined_id = str(element_base_ids).join(str(element_ids).join(str(class_ids)).join(str(object_ids)))
         if combined_id in self.unique_theming_ids:
             return self.unique_theming_ids[combined_id]
 
         combined_ids = []
-        if object_ids is not None and element_ids is not None and class_ids is not None:
-            if len(object_ids) != len(element_ids) or len(class_ids) != len(element_ids):
+        if object_ids is not None and element_ids is not None and class_ids is not None and element_base_ids is not None:
+            if (len(object_ids) != len(element_ids) or len(class_ids) != len(element_ids) or
+                    len(element_base_ids) != len(element_ids)):
                 raise ValueError("Object & class ID hierarchy is not equal "
                                  "in length to Element ID hierarchy"
                                  "Element IDs: " +
@@ -460,7 +472,7 @@ class UIAppearanceTheme(IUIAppearanceThemeInterface):
                                  "\nObject IDs: " +
                                  str(object_ids) + "\n")
             if len(element_ids) != 0:
-                self._get_next_id_node(None, element_ids, class_ids, object_ids,
+                self._get_next_id_node(None, element_base_ids, element_ids, class_ids, object_ids,
                                        0, len(element_ids), combined_ids)
 
             current_ids = combined_ids[:]
diff --git a/pygame_gui/core/ui_element.py b/pygame_gui/core/ui_element.py
index fbaf518..360a805 100644
--- a/pygame_gui/core/ui_element.py
+++ b/pygame_gui/core/ui_element.py
@@ -70,6 +70,7 @@ class UIElement(GUISprite, IUIElementInterface):
         self.object_ids = None
         self.class_ids = None
         self.element_ids = None
+        self.element_base_ids = None
         self.combined_element_ids = None
         self.most_specific_combined_id = 'no_id'
 
@@ -307,6 +308,14 @@ class UIElement(GUISprite, IUIElementInterface):
         """
         return self.rect
 
+    def get_element_base_ids(self) -> List[str]:
+        """
+        A list of all the element base IDs in this element's theming/event hierarchy.
+
+        :return: a list of strings, one for each element in the hierarchy.
+        """
+        return self.element_base_ids
+
     def get_element_ids(self) -> List[str]:
         """
         A list of all the element IDs in this element's theming/event hierarchy.
@@ -335,7 +344,8 @@ class UIElement(GUISprite, IUIElementInterface):
                           container: Union[IContainerLikeInterface, None],
                           parent_element: Union[None, IUIElementInterface],
                           object_id: Union[ObjectID, str, None],
-                          element_id: str):
+                          element_id: str,
+                          element_base_id: Optional[str] = None):
         """
         Creates valid id lists for an element. It will assert if users supply object IDs that
         won't work such as those containing full stops. These ID lists are used by the theming
@@ -369,20 +379,23 @@ class UIElement(GUISprite, IUIElementInterface):
             class_id = None
 
         if id_parent is not None:
+            self.element_base_ids = id_parent.get_element_base_ids().copy()
             self.element_ids = id_parent.get_element_ids().copy()
-            self.element_ids.append(element_id)
-
             self.class_ids = id_parent.get_class_ids().copy()
-            self.class_ids.append(class_id)
-
             self.object_ids = id_parent.get_object_ids().copy()
+
+            self.element_base_ids.append(element_base_id)
+            self.element_ids.append(element_id)
+            self.class_ids.append(class_id)
             self.object_ids.append(obj_id)
         else:
+            self.element_base_ids = [element_base_id]
             self.element_ids = [element_id]
             self.class_ids = [class_id]
             self.object_ids = [obj_id]
 
         self.combined_element_ids = self.ui_manager.get_theme().build_all_combined_ids(
+            self.element_base_ids,
             self.element_ids,
             self.class_ids,
             self.object_ids)
diff --git a/pygame_gui/data/default_theme.json b/pygame_gui/data/default_theme.json
index 504447c..388eaae 100644
--- a/pygame_gui/data/default_theme.json
+++ b/pygame_gui/data/default_theme.json
@@ -287,7 +287,7 @@
             "hover_height": "1"
         }
     },
-    "#confirmation_dialog.text_box":
+    "confirmation_dialog.text_box":
     {
         "misc":
         {
@@ -309,7 +309,7 @@
             "text_horiz_alignment_padding": "0"
         }
     },
-    "#file_dialog.#file_display_list.vertical_scroll_bar":
+    "file_dialog.#file_display_list.vertical_scroll_bar":
     {
         "misc":
         {
@@ -319,7 +319,7 @@
             "enable_arrow_buttons": "0"
         }
     },
-    "#file_dialog.#file_display_list.vertical_scroll_bar.button":
+    "file_dialog.#file_display_list.vertical_scroll_bar.button":
     {
         "misc":
         {
@@ -328,7 +328,7 @@
             "shadow_width": "1"
         }
     },
-    "#file_dialog.#file_display_list.#file_list_item":
+    "file_dialog.#file_display_list.#file_list_item":
     {
         "colours":
         {
@@ -343,7 +343,7 @@
             "text_horiz_alignment_padding": "5"
         }
     },
-    "#file_dialog.#file_display_list.#directory_list_item":
+    "file_dialog.#file_display_list.#directory_list_item":
     {
         "prototype": "#no_frills_button",
         "misc":
@@ -352,11 +352,11 @@
             "text_horiz_alignment_padding": "5"
         }
     },
-    "#file_dialog.#home_icon_button":
+    "file_dialog.#home_icon_button":
     {
         "prototype": "#file_dialog_icon_buttons"
     },
-    "#file_dialog.#delete_icon_button":
+    "file_dialog.#delete_icon_button":
     {
         "prototype": "#file_dialog_icon_buttons",
         "misc":
@@ -364,11 +364,11 @@
             "text_vert_alignment_padding": "0"
         }
     },
-    "#file_dialog.#parent_icon_button":
+    "file_dialog.#parent_icon_button":
     {
         "prototype": "#file_dialog_icon_buttons"
     },
-    "#file_dialog.#refresh_icon_button":
+    "file_dialog.#refresh_icon_button":
     {
         "prototype": "#file_dialog_icon_buttons",
         "misc":
@@ -376,7 +376,7 @@
             "text_vert_alignment_padding": "0"
         }
     },
-    "#message_window.text_box":
+    "message_window.text_box":
     {
         "misc":
         {
@@ -386,7 +386,7 @@
         }
     },
 
-    "#colour_picker_dialog.colour_channel_editor.text_entry_line":
+    "colour_picker_dialog.colour_channel_editor.text_entry_line":
     {
         "prototype": "#default_shape_style",
         "misc":
@@ -394,7 +394,7 @@
             "padding": "2,2"
         }
     },
-    "#colour_picker_dialog.colour_channel_editor.label":
+    "colour_picker_dialog.colour_channel_editor.label":
     {
         "prototype": "#default_shape_style",
         "misc":
@@ -404,7 +404,7 @@
             "text_horiz_alignment_padding": "0"
         }
     },
-    "#colour_picker_dialog.colour_channel_editor.horizontal_slider":
+    "colour_picker_dialog.colour_channel_editor.horizontal_slider":
     {
         "prototype": "#default_shape_style",
 
@@ -414,7 +414,7 @@
             "sliding_button_width": "8"
         }
     },
-    "#colour_picker_dialog.colour_channel_editor.horizontal_slider.#sliding_button":
+    "colour_picker_dialog.colour_channel_editor.horizontal_slider.#sliding_button":
     {
         "prototype": "#no_frills_button",
         "misc":
diff --git a/pygame_gui/elements/ui_drop_down_menu.py b/pygame_gui/elements/ui_drop_down_menu.py
index 72784ed..4a01a9d 100644
--- a/pygame_gui/elements/ui_drop_down_menu.py
+++ b/pygame_gui/elements/ui_drop_down_menu.py
@@ -143,8 +143,11 @@ class UIExpandedDropDownState:
         list_class_ids.append(None)
         list_element_ids = self.drop_down_menu_ui.element_ids[:]
         list_element_ids.append('selection_list')
+        list_base_element_ids = self.drop_down_menu_ui.element_base_ids[:]
+        list_base_element_ids.append(None)
 
-        final_ids = self.ui_manager.get_theme().build_all_combined_ids(list_element_ids,
+        final_ids = self.ui_manager.get_theme().build_all_combined_ids(list_base_element_ids,
+                                                                       list_element_ids,
                                                                        list_class_ids,
                                                                        list_object_ids)
 
diff --git a/pygame_gui/elements/ui_window.py b/pygame_gui/elements/ui_window.py
index 6fe93f6..71ea425 100644
--- a/pygame_gui/elements/ui_window.py
+++ b/pygame_gui/elements/ui_window.py
@@ -57,13 +57,17 @@ class UIWindow(UIElement, IContainerLikeInterface, IWindowInterface):
 
         self.minimum_dimensions = (100, 100)
 
+        base_id = None
         if element_id is None:
             element_id = 'window'
+        else:
+            base_id = 'window'
 
         self._create_valid_ids(container=None,
                                parent_element=None,
                                object_id=object_id,
-                               element_id=element_id)
+                               element_id=element_id,
+                               element_base_id=base_id)
 
         self._set_image(self.ui_manager.get_universal_empty_surface())
         self.bring_to_front_on_focused = True
diff --git a/pygame_gui/windows/ui_colour_picker_dialog.py b/pygame_gui/windows/ui_colour_picker_dialog.py
index e95784a..d5b631d 100644
--- a/pygame_gui/windows/ui_colour_picker_dialog.py
+++ b/pygame_gui/windows/ui_colour_picker_dialog.py
@@ -320,6 +320,7 @@ class UIColourPickerDialog(UIWindow):
 
         super().__init__(rect, manager,
                          window_display_title=window_title,
+                         element_id='colour_picker_dialog',
                          object_id=object_id,
                          resizable=True,
                          visible=visible)
diff --git a/pygame_gui/windows/ui_confirmation_dialog.py b/pygame_gui/windows/ui_confirmation_dialog.py
index 881ef35..2a14a10 100644
--- a/pygame_gui/windows/ui_confirmation_dialog.py
+++ b/pygame_gui/windows/ui_confirmation_dialog.py
@@ -47,6 +47,7 @@ class UIConfirmationDialog(UIWindow):
 
         super().__init__(rect, manager,
                          window_display_title=window_title,
+                         element_id='confirmation_dialog',
                          object_id=object_id,
                          resizable=True,
                          visible=visible)
diff --git a/pygame_gui/windows/ui_console_window.py b/pygame_gui/windows/ui_console_window.py
index cd84db2..6554c3e 100644
--- a/pygame_gui/windows/ui_console_window.py
+++ b/pygame_gui/windows/ui_console_window.py
@@ -37,6 +37,7 @@ class UIConsoleWindow(UIWindow):
                  preload_bold_log_font: bool = True):
         super().__init__(rect, manager,
                          window_display_title=window_title,
+                         element_id='console_window',
                          object_id=object_id,
                          resizable=True,
                          visible=visible)
diff --git a/pygame_gui/windows/ui_file_dialog.py b/pygame_gui/windows/ui_file_dialog.py
index f8590bf..72634d6 100644
--- a/pygame_gui/windows/ui_file_dialog.py
+++ b/pygame_gui/windows/ui_file_dialog.py
@@ -48,6 +48,7 @@ class UIFileDialog(UIWindow):
 
         super().__init__(rect, manager,
                          window_display_title=window_title,
+                         element_id='file_dialog',
                          object_id=object_id,
                          resizable=True,
                          visible=visible)
diff --git a/pygame_gui/windows/ui_message_window.py b/pygame_gui/windows/ui_message_window.py
index e73ea78..6a52137 100644
--- a/pygame_gui/windows/ui_message_window.py
+++ b/pygame_gui/windows/ui_message_window.py
@@ -32,6 +32,7 @@ class UIMessageWindow(UIWindow):
 
         super().__init__(rect, manager,
                          window_display_title=window_title,
+                         element_id='message_window',
                          object_id=object_id,
                          resizable=True,
                          visible=visible)


Test patch:
diff --git a/tests/test_core/test_text/test_html_parser.py b/tests/test_core/test_text/test_html_parser.py
index 63828cc..2a2cccc 100644
--- a/tests/test_core/test_text/test_html_parser.py
+++ b/tests/test_core/test_text/test_html_parser.py
@@ -10,7 +10,8 @@ from pygame_gui.core.text import HTMLParser, LineBreakLayoutRect, ImageLayoutRec
 class TestHTMLParser:
     def test_creation(self, _init_pygame, _display_surface_return_none, default_ui_manager: UIManager):
 
-        combined_ids = default_ui_manager.get_theme().build_all_combined_ids(['text_box'],
+        combined_ids = default_ui_manager.get_theme().build_all_combined_ids([None],
+                                                                             ['text_box'],
                                                                              ['@test_text'],
                                                                              ['#test_text_1'])
         link_style = {'link_text': pygame.Color('#80A0F0'),
@@ -26,7 +27,8 @@ class TestHTMLParser:
         assert len(parser.layout_rect_queue) == 0
 
     def test_handle_start_tag(self, _init_pygame, _display_surface_return_none, default_ui_manager: UIManager):
-        combined_ids = default_ui_manager.get_theme().build_all_combined_ids(['text_box'],
+        combined_ids = default_ui_manager.get_theme().build_all_combined_ids([None],
+                                                                             ['text_box'],
                                                                              ['@test_text'],
                                                                              ['#test_text_1'])
         link_style = {'link_text': pygame.Color('#80A0F0'),
@@ -105,7 +107,8 @@ class TestHTMLParser:
         assert parser.layout_rect_queue[-1].padding.left == 0
 
     def test_handle_end_tag(self, _init_pygame, _display_surface_return_none, default_ui_manager: UIManager):
-        combined_ids = default_ui_manager.get_theme().build_all_combined_ids(['text_box'],
+        combined_ids = default_ui_manager.get_theme().build_all_combined_ids([None],
+                                                                             ['text_box'],
                                                                              ['@test_text'],
                                                                              ['#test_text_1'])
         link_style = {'link_text': pygame.Color('#80A0F0'),
@@ -133,7 +136,8 @@ class TestHTMLParser:
         assert parser.current_style['bold'] is False
 
     def test_handle_data(self, _init_pygame, _display_surface_return_none, default_ui_manager: UIManager):
-        combined_ids = default_ui_manager.get_theme().build_all_combined_ids(['text_box'],
+        combined_ids = default_ui_manager.get_theme().build_all_combined_ids([None],
+                                                                             ['text_box'],
                                                                              ['@test_text'],
                                                                              ['#test_text_1'])
         link_style = {'link_text': pygame.Color('#80A0F0'),
@@ -165,7 +169,8 @@ class TestHTMLParser:
         assert parser.layout_rect_queue[1].underlined is True
 
     def test_push_style(self, _init_pygame, _display_surface_return_none, default_ui_manager: UIManager):
-        combined_ids = default_ui_manager.get_theme().build_all_combined_ids(['text_box'],
+        combined_ids = default_ui_manager.get_theme().build_all_combined_ids([None],
+                                                                             ['text_box'],
                                                                              ['@test_text'],
                                                                              ['#test_text_1'])
         link_style = {'link_text': pygame.Color('#80A0F0'),
@@ -187,7 +192,8 @@ class TestHTMLParser:
         assert parser.current_style['bold'] is True
 
     def test_pop_style(self, _init_pygame, _display_surface_return_none, default_ui_manager: UIManager):
-        combined_ids = default_ui_manager.get_theme().build_all_combined_ids(['text_box'],
+        combined_ids = default_ui_manager.get_theme().build_all_combined_ids([None],
+                                                                             ['text_box'],
                                                                              ['@test_text'],
                                                                              ['#test_text_1'])
         link_style = {'link_text': pygame.Color('#80A0F0'),
@@ -216,7 +222,8 @@ class TestHTMLParser:
         parser.pop_style('bad_tag')
 
     def test_error(self, _init_pygame, _display_surface_return_none, default_ui_manager: UIManager):
-        combined_ids = default_ui_manager.get_theme().build_all_combined_ids(['text_box'],
+        combined_ids = default_ui_manager.get_theme().build_all_combined_ids([None],
+                                                                             ['text_box'],
                                                                              ['@test_text'],
                                                                              ['#test_text_1'])
         link_style = {'link_text': pygame.Color('#80A0F0'),
diff --git a/tests/test_core/test_text/test_text_box_layout_row.py b/tests/test_core/test_text/test_text_box_layout_row.py
index 1cf3cc5..f5e6625 100644
--- a/tests/test_core/test_text/test_text_box_layout_row.py
+++ b/tests/test_core/test_text/test_text_box_layout_row.py
@@ -620,7 +620,8 @@ class TestTextBoxLayoutRow:
         assert layout_row.cursor_draw_width == 44
 
     def test_insert_text(self, _init_pygame, _display_surface_return_none, default_ui_manager: UIManager):
-        combined_ids = default_ui_manager.get_theme().build_all_combined_ids(['text_box'],
+        combined_ids = default_ui_manager.get_theme().build_all_combined_ids([None],
+                                                                             ['text_box'],
                                                                              ['@test_text'],
                                                                              ['#test_text_1'])
         link_style = {'link_text': pygame.Color('#80A0F0'),
diff --git a/tests/test_core/test_ui_appearance_theme.py b/tests/test_core/test_ui_appearance_theme.py
index 3edbdf3..9c77fa5 100644
--- a/tests/test_core/test_ui_appearance_theme.py
+++ b/tests/test_core/test_ui_appearance_theme.py
@@ -111,7 +111,8 @@ class TestUIAppearanceTheme:
         theme = UIAppearanceTheme(BlockingThreadedResourceLoader(), locale='en')
         with pytest.raises(ValueError, match="Object & class ID hierarchy is not "
                                              "equal in length to Element ID hierarchy"):
-            theme.build_all_combined_ids(element_ids=['button'],
+            theme.build_all_combined_ids(element_base_ids=[None],
+                                         element_ids=['button'],
                                          class_ids=[None],
                                          object_ids=['whut', 'the', 'heck'])
 
@@ -140,7 +141,8 @@ class TestUIAppearanceTheme:
         theme.load_theme(PackageResource('tests.data.themes',
                                          'appearance_theme_class_id_test.json'))
 
-        combined_ids = theme.build_all_combined_ids(element_ids=['button'],
+        combined_ids = theme.build_all_combined_ids(element_base_ids=[None],
+                                                    element_ids=['button'],
                                                     class_ids=['@test_class'],
                                                     object_ids=['#test_object_2'])
 
@@ -153,7 +155,8 @@ class TestUIAppearanceTheme:
         assert shadow_width == '0'
         assert border_width == '3'
 
-        combined_ids = theme.build_all_combined_ids(element_ids=['button'],
+        combined_ids = theme.build_all_combined_ids(element_base_ids=[None],
+                                                    element_ids=['button'],
                                                     class_ids=['@test_class'],
                                                     object_ids=['#test_object_1'])
 
@@ -167,7 +170,8 @@ class TestUIAppearanceTheme:
         assert border_width == '1'
         assert shadow_width == '3'
 
-        combined_ids = theme.build_all_combined_ids(element_ids=['button'], class_ids=[None],
+        combined_ids = theme.build_all_combined_ids(element_base_ids=[None],
+                                                    element_ids=['button'], class_ids=[None],
                                                     object_ids=['#test_object_2'])
 
         assert combined_ids == ['#test_object_2', 'button']