Repository: MyreMylar/pygame_gui (python)

Issue:
Add 'always on top' feature to windows & window stack
**Is your feature request related to a problem? Please describe.**

Allow any window to be set as 'always on top' so it will be drawn above all windows in the normal window stack. This will need a second 'always on top' stack above the normal window stack where the bottom window in that stack is drawn above the top window in the normal stack..

Powertoys for Windows adds this functionality to windows. 


Pull request:
Add always on top feature to windows
fixes #564

Patch:
diff --git a/pygame_gui/core/interfaces/window_interface.py b/pygame_gui/core/interfaces/window_interface.py
index ea72445..2ed6162 100644
--- a/pygame_gui/core/interfaces/window_interface.py
+++ b/pygame_gui/core/interfaces/window_interface.py
@@ -7,13 +7,37 @@ import pygame
 
 class IWindowInterface(metaclass=ABCMeta):
     """
-    A meta class that defines the interface that the window stack uses to interface with the
+    A metaclass that defines the interface that the window stack uses to interface with the
     UIWindow class.
 
     Interfaces like this help us evade cyclical import problems by allowing us to define the
     actual window class later on and have it make use of the window stack.
     """
 
+    @property
+    @abstractmethod
+    def layer(self) -> int:
+        """
+        The layer of this window (read-only)
+        """
+
+    @property
+    @abstractmethod
+    def always_on_top(self) -> bool:
+        """
+        Whether the window is always above normal windows or not.
+        :return:
+        """
+
+    @always_on_top.setter
+    @abstractmethod
+    def always_on_top(self, value: bool):
+        """
+        Sets whether the window is always above normal windows or not.
+        :param value: the value to set
+
+        """
+
     @abstractmethod
     def set_blocking(self, state: bool):
         """
@@ -79,7 +103,7 @@ class IWindowInterface(metaclass=ABCMeta):
     @abstractmethod
     def check_clicked_inside_or_blocking(self, event: pygame.event.Event) -> bool:
         """
-        A quick event check outside of the normal event processing so that this window is brought
+        A quick event check outside the normal event processing so that this window is brought
         to the front of the window stack if we click on any of the elements contained within it.
 
         :param event: The event to check.
@@ -112,7 +136,7 @@ class IWindowInterface(metaclass=ABCMeta):
         For the window the only hovering we care about is the edges if this is a resizable window.
 
         :param time_delta: time passed in seconds between one call to this method and the next.
-        :param hovered_higher_element: Have we already hovered an element/window above this one.
+        :param hovered_higher_element: Have we already hovered an element/window above this one?
 
         """
 
@@ -186,3 +210,9 @@ class IWindowInterface(metaclass=ABCMeta):
 
         :param new_title: The title to set.
         """
+
+    def get_layer_thickness(self) -> int:
+        """
+        The layer 'thickness' of this window/
+        :return: an integer
+        """
diff --git a/pygame_gui/core/interfaces/window_stack_interface.py b/pygame_gui/core/interfaces/window_stack_interface.py
index a95802a..ef4578a 100644
--- a/pygame_gui/core/interfaces/window_stack_interface.py
+++ b/pygame_gui/core/interfaces/window_stack_interface.py
@@ -57,10 +57,20 @@ class IUIWindowStackInterface(metaclass=ABCMeta):
 
         """
 
+    def is_window_at_top_of_top(self, window: IWindowInterface) -> bool:
+        """
+        Checks if a window is at the top of the top window stack or not.
+
+        :param window: The window to check.
+
+        :return: returns True if this window is at the top of the stack.
+
+        """
+
     @abstractmethod
-    def get_stack(self) -> List[IWindowInterface]:
+    def get_full_stack(self) -> List[IWindowInterface]:
         """
-        Return the internal window stack directly.
+        Returns the full stack of normal and always on top windows.
 
         :return: a list of Windows
         """
diff --git a/pygame_gui/core/ui_container.py b/pygame_gui/core/ui_container.py
index 978b844..67a4ffc 100644
--- a/pygame_gui/core/ui_container.py
+++ b/pygame_gui/core/ui_container.py
@@ -124,7 +124,7 @@ class UIContainer(UIElement, IUIContainerInterface, IContainerLikeInterface):
         max_element_top_layer = self._layer
         for element in self.elements:
             if (element.get_top_layer() > max_element_top_layer
-                    and element not in self.ui_manager.get_window_stack().get_stack()
+                    and element not in self.ui_manager.get_window_stack().get_full_stack()
                     and (not isinstance(element, UIContainer) or
                          not element.is_window_root_container)):
                 max_element_top_layer = element.get_top_layer()
@@ -143,7 +143,7 @@ class UIContainer(UIElement, IUIContainerInterface, IContainerLikeInterface):
         :param element: the element to check.
         """
         if (element.get_top_layer() > self.max_element_top_layer
-                and element not in self.ui_manager.get_window_stack().get_stack()
+                and element not in self.ui_manager.get_window_stack().get_full_stack()
                 and (not isinstance(element, UIContainer) or not element.is_window_root_container)):
 
             self.max_element_top_layer = element.get_top_layer()
diff --git a/pygame_gui/core/ui_window_stack.py b/pygame_gui/core/ui_window_stack.py
index 8b3d536..1401107 100644
--- a/pygame_gui/core/ui_window_stack.py
+++ b/pygame_gui/core/ui_window_stack.py
@@ -15,7 +15,10 @@ class UIWindowStack(IUIWindowStackInterface):
     """
     def __init__(self, window_resolution: Tuple[int, int], root_container: IUIContainerInterface):
         self.window_resolution = window_resolution
-        self.stack = []  # type: List[IWindowInterface]
+        self.stack: List[IWindowInterface] = []  # the main stack
+        # a second stack that sits above the first,
+        # contains 'always_on_top' windows
+        self.top_stack: List[IWindowInterface] = []
         self.root_container = root_container
 
     def clear(self):
@@ -26,25 +29,88 @@ class UIWindowStack(IUIWindowStackInterface):
             self.stack.pop().kill()
         self.stack.clear()
 
+        while len(self.top_stack) != 0:
+            self.top_stack.pop().kill()
+        self.top_stack.clear()
+
     def add_new_window(self, window: IWindowInterface):
         """
-        Adds a window to the top of the stack.
+        Adds a new window to the top of the stack.
 
         :param window: The window to add.
 
         """
-        new_layer = (self.stack[-1].get_top_layer() + 1
-                     if len(self.stack) > 0
-                     else self.root_container.get_top_layer() + 1)
+        if window.always_on_top:
+            new_layer = (self.top_stack[-1].get_top_layer() + 1
+                         if len(self.top_stack) > 0
+                         else (self.stack[-1].get_top_layer() + 1
+                         if len(self.stack) > 0
+                         else self.root_container.get_top_layer() + 1))
+
+            window.change_layer(new_layer)
+            self.top_stack.append(window)
+            window.on_moved_to_front()
+        else:
+            new_layer = (self.stack[-1].get_top_layer() + 1
+                         if len(self.stack) > 0
+                         else self.root_container.get_top_layer() + 1)
+
+            window.change_layer(new_layer)
+            self.stack.append(window)
+            window.on_moved_to_front()
+
+            increased_height = window.get_layer_thickness()
+
+            # need to bump up the layers of everything in the top stack as the main stack just got bigger
+            for window in self.top_stack:
+                window.change_layer(window.layer + increased_height)
+
+    def refresh_window_stack_from_window(self, window_to_refresh_from: IWindowInterface):
+        if window_to_refresh_from in self.stack:
+            popped_windows_to_add_back = []
+
+            # first clear out the top stack
+            try:
+                top_window = self.top_stack.pop()
+                while top_window is not None:
+                    popped_windows_to_add_back.append(top_window)
+                    try:
+                        top_window = self.top_stack.pop()
+                    except IndexError:
+                        top_window = None
+            except IndexError:
+                pass
+
+            # then clear out everything above our target
+            window = self.stack.pop()
+            while window != window_to_refresh_from:
+                popped_windows_to_add_back.append(window)
+                window = self.stack.pop()
 
-        window.change_layer(new_layer)
-        self.stack.append(window)
-        window.on_moved_to_front()
+            # add back our target as well
+            popped_windows_to_add_back.append(window_to_refresh_from)
+
+            # reverse the list and re-add them
+            popped_windows_to_add_back.reverse()
+            for old_window in popped_windows_to_add_back:
+                self.add_new_window(old_window)
+
+        elif window_to_refresh_from in self.top_stack:
+            popped_windows_to_add_back = []
+            window = self.top_stack.pop()
+            while window != window_to_refresh_from:
+                popped_windows_to_add_back.append(window)
+                window = self.top_stack.pop()
+            popped_windows_to_add_back.append(window_to_refresh_from)
+
+            popped_windows_to_add_back.reverse()
+            for old_window in popped_windows_to_add_back:
+                self.add_new_window(old_window)
 
     def remove_window(self, window_to_remove: IWindowInterface):
         """
         Removes a window from the stack and resorts the remaining windows to adjust for
-        it's absence.
+        its absence.
 
         :param window_to_remove: the window to remove.
 
@@ -56,18 +122,56 @@ class UIWindowStack(IUIWindowStackInterface):
                 popped_windows_to_add_back.append(window)
                 window = self.stack.pop()
 
+            try:
+                top_window = self.top_stack.pop()
+                while top_window is not None:
+                    popped_windows_to_add_back.append(top_window)
+                    try:
+                        top_window = self.top_stack.pop()
+                    except IndexError:
+                        top_window = None
+            except IndexError:
+                pass
+
+            popped_windows_to_add_back.reverse()
+            for old_window in popped_windows_to_add_back:
+                self.add_new_window(old_window)
+
+        elif window_to_remove in self.top_stack:
+            popped_windows_to_add_back = []
+            window = self.top_stack.pop()
+            while window != window_to_remove:
+                popped_windows_to_add_back.append(window)
+                window = self.top_stack.pop()
+
             popped_windows_to_add_back.reverse()
             for old_window in popped_windows_to_add_back:
                 self.add_new_window(old_window)
 
     def move_window_to_front(self, window_to_front: IWindowInterface):
         """
-        Moves the passed in window to the top of the window stack and resorts the other windows
+        Moves the passed in window to the top of its stack and resorts the other windows
         to deal with the change.
 
         :param window_to_front: the window to move to the front.
 
         """
+        if window_to_front in self.top_stack:
+            if window_to_front == self.top_stack[-1]:
+                return  # already at top of top stack
+            popped_windows_to_add_back = []
+            window = self.top_stack.pop()
+            while window != window_to_front:
+                popped_windows_to_add_back.append(window)
+                window = self.top_stack.pop()
+
+            popped_windows_to_add_back.reverse()
+            for old_window in popped_windows_to_add_back:
+                self.add_new_window(old_window)
+
+            self.add_new_window(window_to_front)
+            window_to_front.on_moved_to_front()
+
         if window_to_front not in self.stack or window_to_front == self.stack[-1]:
             return
         popped_windows_to_add_back = []
@@ -85,7 +189,7 @@ class UIWindowStack(IUIWindowStackInterface):
 
     def is_window_at_top(self, window: IWindowInterface) -> bool:
         """
-        Checks if a window is at the top of the window stack or not.
+        Checks if a window is at the top of the normal window stack or not.
 
         :param window: The window to check.
 
@@ -94,10 +198,21 @@ class UIWindowStack(IUIWindowStackInterface):
         """
         return window is self.stack[-1]
 
-    def get_stack(self) -> List[IWindowInterface]:
+    def is_window_at_top_of_top(self, window: IWindowInterface) -> bool:
+        """
+        Checks if a window is at the top of the top window stack or not.
+
+        :param window: The window to check.
+
+        :return: returns True if this window is at the top of the stack.
+
+        """
+        return window is self.top_stack[-1]
+
+    def get_full_stack(self) -> List[IWindowInterface]:
         """
-        Return the internal window stack directly.
+        Returns the full stack of normal and always on top windows.
 
         :return: a list of Windows
         """
-        return self.stack
+        return self.stack + self.top_stack
diff --git a/pygame_gui/elements/ui_window.py b/pygame_gui/elements/ui_window.py
index b26280d..bc121ae 100644
--- a/pygame_gui/elements/ui_window.py
+++ b/pygame_gui/elements/ui_window.py
@@ -44,12 +44,14 @@ class UIWindow(UIElement, IContainerLikeInterface, IWindowInterface):
                  visible: int = 1,
                  draggable: bool = True,
                  *,
-                 ignore_shadow_for_initial_size_and_pos: bool = True):
+                 ignore_shadow_for_initial_size_and_pos: bool = True,
+                 always_on_top: bool = False):
 
         self.window_display_title = window_display_title
         self._window_root_container = None  # type: Optional[UIContainer]
         self.resizable = resizable
         self.draggable = draggable
+        self._always_on_top = always_on_top
 
         self.edge_hovering = [False, False, False, False]
 
@@ -99,6 +101,17 @@ class UIWindow(UIElement, IContainerLikeInterface, IWindowInterface):
         self.window_stack = self.ui_manager.get_window_stack()
         self.window_stack.add_new_window(self)
 
+    @property
+    def always_on_top(self) -> bool:
+        return self._always_on_top
+
+    @always_on_top.setter
+    def always_on_top(self, value: bool):
+        if value != self._always_on_top:
+            self.window_stack.remove_window(self)
+            self._always_on_top = value
+            self.window_stack.add_new_window(self)
+
     def set_blocking(self, state: bool):
         """
         Sets whether this window being open should block clicks to the rest of the UI or not.
@@ -252,6 +265,7 @@ class UIWindow(UIElement, IContainerLikeInterface, IWindowInterface):
         # This is needed to keep the window in sync with the container after adding elements to it
         if self._window_root_container.layer_thickness != self.layer_thickness:
             self.layer_thickness = self._window_root_container.layer_thickness
+            self.window_stack.refresh_window_stack_from_window(self)
         if self.title_bar is not None:
             if self.title_bar.held:
                 mouse_x, mouse_y = self.ui_manager.get_mouse_position()
@@ -677,7 +691,7 @@ class UIWindow(UIElement, IContainerLikeInterface, IWindowInterface):
 
     def on_moved_to_front(self):
         """
-        Called when a window is moved to the front of the stack.
+        Called when a window is moved to the front of its stack.
         """
         # old event - to be removed in 0.8.0
         window_front_event = pygame.event.Event(pygame.USEREVENT,
@@ -689,6 +703,7 @@ class UIWindow(UIElement, IContainerLikeInterface, IWindowInterface):
         # new event
         window_front_event = pygame.event.Event(UI_WINDOW_MOVED_TO_FRONT,
                                                 {'ui_element': self,
+                                                 'always_on_top': int(self.always_on_top),
                                                  'ui_object_id': self.most_specific_combined_id})
         pygame.event.post(window_front_event)
 
@@ -786,3 +801,10 @@ class UIWindow(UIElement, IContainerLikeInterface, IWindowInterface):
             if any_hovered:
                 break
         return any_hovered
+
+    def get_layer_thickness(self) -> int:
+        """
+        The layer 'thickness' of this window/
+        :return: an integer
+        """
+        return self.layer_thickness
diff --git a/pygame_gui/windows/ui_colour_picker_dialog.py b/pygame_gui/windows/ui_colour_picker_dialog.py
index a6b6257..9f7d365 100644
--- a/pygame_gui/windows/ui_colour_picker_dialog.py
+++ b/pygame_gui/windows/ui_colour_picker_dialog.py
@@ -311,14 +311,16 @@ class UIColourPickerDialog(UIWindow):
                  initial_colour: pygame.Color = pygame.Color(0, 0, 0, 255),
                  window_title: str = "pygame-gui.colour_picker_title_bar",
                  object_id: Union[ObjectID, str] = ObjectID('#colour_picker_dialog', None),
-                 visible: int = 1):
+                 visible: int = 1,
+                 always_on_top: bool = False):
 
         super().__init__(rect, manager,
                          window_display_title=window_title,
                          element_id=['colour_picker_dialog'],
                          object_id=object_id,
                          resizable=True,
-                         visible=visible)
+                         visible=visible,
+                         always_on_top=always_on_top)
 
         minimum_dimensions = (390, 390)
         if self.relative_rect.width < minimum_dimensions[0] or self.relative_rect.height < minimum_dimensions[1]:
diff --git a/pygame_gui/windows/ui_confirmation_dialog.py b/pygame_gui/windows/ui_confirmation_dialog.py
index fcf06e7..5ec7a50 100644
--- a/pygame_gui/windows/ui_confirmation_dialog.py
+++ b/pygame_gui/windows/ui_confirmation_dialog.py
@@ -43,7 +43,8 @@ class UIConfirmationDialog(UIWindow):
                  blocking: bool = True,
                  object_id: Union[ObjectID, str] = ObjectID('#confirmation_dialog', None),
                  visible: int = 1,
-                 action_long_desc_text_kwargs: Optional[Dict[str, str]] = None
+                 action_long_desc_text_kwargs: Optional[Dict[str, str]] = None,
+                 always_on_top: bool = False
                  ):
 
         super().__init__(rect, manager,
@@ -51,7 +52,8 @@ class UIConfirmationDialog(UIWindow):
                          element_id=['confirmation_dialog'],
                          object_id=object_id,
                          resizable=True,
-                         visible=visible)
+                         visible=visible,
+                         always_on_top=always_on_top)
 
         minimum_dimensions = (260, 200)
         if self.relative_rect.width < minimum_dimensions[0] or self.relative_rect.height < minimum_dimensions[1]:
diff --git a/pygame_gui/windows/ui_console_window.py b/pygame_gui/windows/ui_console_window.py
index f7aa879..cee37f2 100644
--- a/pygame_gui/windows/ui_console_window.py
+++ b/pygame_gui/windows/ui_console_window.py
@@ -35,13 +35,15 @@ class UIConsoleWindow(UIWindow):
                  window_title: str = 'pygame-gui.console_title_bar',
                  object_id: Union[ObjectID, str] = ObjectID('#console_window', None),
                  visible: int = 1,
-                 preload_bold_log_font: bool = True):
+                 preload_bold_log_font: bool = True,
+                 always_on_top: bool = False):
         super().__init__(rect, manager,
                          window_display_title=window_title,
                          element_id=['console_window'],
                          object_id=object_id,
                          resizable=True,
-                         visible=visible)
+                         visible=visible,
+                         always_on_top=always_on_top)
 
         self.default_log_prefix = '> '
         self.log_prefix = self.default_log_prefix
diff --git a/pygame_gui/windows/ui_file_dialog.py b/pygame_gui/windows/ui_file_dialog.py
index e115288..f250fc6 100644
--- a/pygame_gui/windows/ui_file_dialog.py
+++ b/pygame_gui/windows/ui_file_dialog.py
@@ -45,7 +45,8 @@ class UIFileDialog(UIWindow):
                  object_id: Union[ObjectID, str] = ObjectID('#file_dialog', None),
                  allow_existing_files_only: bool = False,
                  allow_picking_directories: bool = False,
-                 visible: int = 1
+                 visible: int = 1,
+                 always_on_top: bool = False
                  ):
 
         super().__init__(rect, manager,
@@ -53,7 +54,8 @@ class UIFileDialog(UIWindow):
                          element_id=['file_dialog'],
                          object_id=object_id,
                          resizable=True,
-                         visible=visible)
+                         visible=visible,
+                         always_on_top=always_on_top)
 
         locale.setlocale(locale.LC_ALL, "")
 
diff --git a/pygame_gui/windows/ui_message_window.py b/pygame_gui/windows/ui_message_window.py
index a373de1..74dfd48 100644
--- a/pygame_gui/windows/ui_message_window.py
+++ b/pygame_gui/windows/ui_message_window.py
@@ -29,14 +29,16 @@ class UIMessageWindow(UIWindow):
                  window_title: str = 'pygame-gui.message_window_title_bar',
                  object_id: Union[ObjectID, str] = ObjectID('#message_window', None),
                  visible: int = 1,
-                 html_message_text_kwargs: Optional[Dict[str, str]] = None):
+                 html_message_text_kwargs: Optional[Dict[str, str]] = None,
+                 always_on_top: bool = False):
 
         super().__init__(rect, manager,
                          window_display_title=window_title,
                          element_id=['message_window'],
                          object_id=object_id,
                          resizable=True,
-                         visible=visible)
+                         visible=visible,
+                         always_on_top=always_on_top)
 
         minimum_dimensions = (250, 160)
         if self.relative_rect.width < minimum_dimensions[0] or self.relative_rect.height < minimum_dimensions[1]:


Test patch:
diff --git a/tests/test_core/test_window_stack.py b/tests/test_core/test_window_stack.py
index b98eabc..c97a369 100644
--- a/tests/test_core/test_window_stack.py
+++ b/tests/test_core/test_window_stack.py
@@ -3,6 +3,8 @@ import pygame
 
 from pygame_gui.core.ui_window_stack import UIWindowStack
 from pygame_gui.elements.ui_window import UIWindow
+from pygame_gui.elements.ui_drop_down_menu import UIDropDownMenu
+from pygame_gui.ui_manager import UIManager
 
 
 class TestWindowStack:
@@ -88,6 +90,157 @@ class TestWindowStack:
         assert stack.is_window_at_top(window) is False
         assert stack.is_window_at_top(window_3) is False
 
+    def test_add_window_always_on_top(self, _init_pygame, default_ui_manager,
+                                      _display_surface_return_none):
+        stack = UIWindowStack((800, 600), default_ui_manager.get_root_container())
+        window = UIWindow(pygame.Rect(100, 100, 200, 200), window_display_title="Test Window",
+                          manager=default_ui_manager, element_id='test_window')
+        window_always_on_top = UIWindow(pygame.Rect(100, 100, 200, 200), window_display_title="Test Window",
+                                        manager=default_ui_manager, element_id='test_window',
+                                        always_on_top=True)
+        stack.add_new_window(window)
+        stack.add_new_window(window_always_on_top)
+
+        assert len(stack.stack) == 1
+        assert len(stack.top_stack) == 1
+
+    def test_clear_with_always_on_top(self, _init_pygame, default_ui_manager,
+                                      _display_surface_return_none):
+        stack = UIWindowStack((800, 600), default_ui_manager.get_root_container())
+        window = UIWindow(pygame.Rect(100, 100, 200, 200), window_display_title="Test Window",
+                          manager=default_ui_manager, element_id='test_window')
+        window_always_on_top = UIWindow(pygame.Rect(0, 0, 200, 200), window_display_title="Test Window",
+                                        manager=default_ui_manager, element_id='test_window',
+                                        always_on_top=True)
+        stack.add_new_window(window)
+        stack.add_new_window(window_always_on_top)
+
+        assert len(stack.stack) == 1
+        assert len(stack.top_stack) == 1
+
+        stack.clear()
+
+        assert len(stack.stack) == 0
+        assert len(stack.top_stack) == 0
+
+    def test_remove_window_with_always_on_top(self, _init_pygame, default_ui_manager,
+                                              _display_surface_return_none):
+        stack = UIWindowStack((800, 600), default_ui_manager.get_root_container())
+        window_always_on_top = UIWindow(pygame.Rect(0, 0, 200, 200), window_display_title="Test Window",
+                                        manager=default_ui_manager, element_id='test_window',
+                                        always_on_top=True)
+        window = UIWindow(pygame.Rect(100, 100, 200, 200), window_display_title="Test Window",
+                          manager=default_ui_manager, element_id='test_window')
+        window_2 = UIWindow(pygame.Rect(50, 50, 200, 200), window_display_title="Test Window",
+                            manager=default_ui_manager, element_id='test_window')
+        window_3 = UIWindow(pygame.Rect(0, 0, 200, 200), window_display_title="Test Window",
+                            manager=default_ui_manager, element_id='test_window')
+        window_always_on_top_2 = UIWindow(pygame.Rect(0, 0, 200, 200), window_display_title="Test Window",
+                                          manager=default_ui_manager, element_id='test_window',
+                                          always_on_top=True)
+        stack.add_new_window(window_always_on_top)
+        stack.add_new_window(window)
+        stack.add_new_window(window_2)
+        stack.add_new_window(window_3)
+        stack.add_new_window(window_always_on_top_2)
+        assert len(stack.stack) == 3
+        assert len(stack.top_stack) == 2
+        assert stack.is_window_at_top(window_3)
+        assert stack.is_window_at_top_of_top(window_always_on_top_2)
+        stack.remove_window(window)
+        stack.remove_window(window_always_on_top)
+        stack.remove_window(window_2)
+        stack.remove_window(window_3)
+        stack.remove_window(window_always_on_top_2)
+
+        assert len(stack.top_stack) == 0
+        assert len(stack.stack) == 0
+
+    def test_move_window_to_front_with_always_on_top(self, _init_pygame, default_ui_manager,
+                                                     _display_surface_return_none):
+        stack = UIWindowStack((800, 600), default_ui_manager.get_root_container())
+        window_always_on_top = UIWindow(pygame.Rect(0, 0, 200, 200), window_display_title="Test Window",
+                                        manager=default_ui_manager, element_id='test_window',
+                                        always_on_top=True)
+        window_always_on_top_2 = UIWindow(pygame.Rect(0, 0, 200, 200), window_display_title="Test Window",
+                                          manager=default_ui_manager, element_id='test_window',
+                                          always_on_top=True)
+        window = UIWindow(pygame.Rect(100, 100, 200, 200), window_display_title="Test Window",
+                          manager=default_ui_manager, element_id='test_window')
+        window_2 = UIWindow(pygame.Rect(50, 50, 200, 200), window_display_title="Test Window",
+                            manager=default_ui_manager, element_id='test_window')
+        window_3 = UIWindow(pygame.Rect(0, 0, 200, 200), window_display_title="Test Window",
+                            manager=default_ui_manager, element_id='test_window')
+
+        stack.add_new_window(window_always_on_top)
+        stack.add_new_window(window_always_on_top_2)
+        stack.add_new_window(window)
+        stack.add_new_window(window_2)
+        stack.add_new_window(window_3)
+        stack.move_window_to_front(window)
+        stack.move_window_to_front(window_3)
+        stack.move_window_to_front(window_2)
+        stack.move_window_to_front(window_always_on_top)
+
+        assert stack.stack[0] == window
+        assert stack.stack[2] == window_2
+        assert stack.top_stack[0] == window_always_on_top_2
+        assert stack.top_stack[1] == window_always_on_top
+        assert window_always_on_top_2.layer > window_2.layer
+
+    def test_swap_window_always_on_top(self, _init_pygame, default_ui_manager,
+                                       _display_surface_return_none):
+        manager = UIManager((800, 600))
+        stack = manager.ui_window_stack
+        window = UIWindow(pygame.Rect(100, 100, 200, 200), window_display_title="Test Window",
+                          manager=manager, element_id='test_window')
+        window_always_on_top = UIWindow(pygame.Rect(100, 100, 200, 200), window_display_title="Test Window",
+                                        manager=manager, element_id='test_window',
+                                        always_on_top=True)
+
+        window_always_on_top.always_on_top = False
+        window.always_on_top = True
+
+        assert len(stack.stack) == 1
+        assert len(stack.top_stack) == 1
+        assert window == stack.top_stack[0]
+        assert window_always_on_top == stack.stack[0]
+
+    def test_update_always_on_top(self, _init_pygame, default_ui_manager,
+                                  _display_surface_return_none):
+        manager = UIManager((800, 600))
+
+        window = UIWindow(pygame.Rect(100, 100, 200, 200), window_display_title="Test Window",
+                          manager=manager, element_id='test_window')
+        assert manager.get_window_stack().get_full_stack()[-1].get_top_layer() == 5
+        UIWindow(pygame.Rect(100, 100, 200, 200), window_display_title="Test Window",
+                 manager=manager, element_id='test_window')
+        assert manager.get_window_stack().get_full_stack()[-1].get_top_layer() == 8
+        window_always_on_top = UIWindow(pygame.Rect(100, 100, 200, 200), window_display_title="Test Window",
+                                        manager=manager, element_id='test_window', always_on_top=True)
+        assert manager.get_window_stack().get_full_stack()[-1].get_top_layer() == 11
+        window_always_on_top_2 = UIWindow(pygame.Rect(100, 100, 200, 200), window_display_title="Test Window",
+                                          manager=manager, element_id='test_window', always_on_top=True)
+
+        assert manager.get_window_stack().get_full_stack()[-1].get_top_layer() == 14
+
+        assert window_always_on_top_2.get_top_layer() == 14
+        assert window.layer_thickness == 2
+
+        UIDropDownMenu(["Test", "Test"], "Test", (50, 50, 120, 30), container=window, manager=manager)
+        assert window.layer_thickness == 2
+        window.update(0.4)
+        assert window.layer_thickness == 4
+        assert manager.get_window_stack().get_full_stack()[-1].get_top_layer() == 16
+        assert window_always_on_top_2.get_top_layer() == 16
+
+        UIDropDownMenu(["Test", "Test"], "Test", (50, 50, 120, 30), container=window_always_on_top, manager=manager)
+        assert window_always_on_top.layer_thickness == 2
+        window_always_on_top.update(0.4)
+        assert window_always_on_top.layer_thickness == 4
+        assert manager.get_window_stack().get_full_stack()[-1].get_top_layer() == 18
+        assert window_always_on_top_2.get_top_layer() == 18
+
 
 if __name__ == '__main__':
     pytest.console_main()