Repository: ppizarror/pygame-menu

Issue:
Query the size of the menu including the border
A related thing that would be useful is a way to query the size of the menu including the border, for placement purposes.

_Originally posted by @vnmabus in https://github.com/ppizarror/pygame-menu/issues/390#issuecomment-1079982395_

Comments:

Patch:
diff --git a/pygame_menu/_scrollarea.py b/pygame_menu/_scrollarea.py
index f0d8d3cc..b5cf6c34 100644
--- a/pygame_menu/_scrollarea.py
+++ b/pygame_menu/_scrollarea.py
@@ -574,6 +574,19 @@ class ScrollArea(Base):
 
         return self
 
+    def get_border_size(self) -> Tuple2IntType:
+        """
+        Return the border size (width, height) in px.
+
+        :return: Border width, height
+        """
+        if isinstance(self._border_color, pygame_menu.BaseImage):  # Image
+            return self._border_tiles_size
+        else:  # Color
+            if self._border_color is None:
+                return 0, 0
+            return self._border_width, self._border_width
+
     def get_hidden_width(self) -> int:
         """
         Return the total width out of the bounds of the viewable area.
diff --git a/pygame_menu/menu.py b/pygame_menu/menu.py
index 47d7b85e..99f76123 100644
--- a/pygame_menu/menu.py
+++ b/pygame_menu/menu.py
@@ -1835,7 +1835,7 @@ class Menu(Base):
         return self._scrollarea.get_scrollbar_thickness(ORIENTATION_HORIZONTAL), \
                self._scrollarea.get_scrollbar_thickness(ORIENTATION_VERTICAL)
 
-    def get_width(self, inner: bool = False, widget: bool = False) -> int:
+    def get_width(self, inner: bool = False, widget: bool = False, border: bool = False) -> int:
         """
         Get the Menu width.
 
@@ -1847,15 +1847,17 @@ class Menu(Base):
 
         :param inner: If ``True`` returns the available width (menu width minus scroll if visible)
         :param widget: If ``True`` returns the total width used by the widgets
+        :param border: If ``True`` add the mmenu border width. Only applied if both ``inner`` and ``widget`` are ``False``
         :return: Width in px
         """
         if widget:
             return int(self._widget_max_position[0] - self._widget_min_position[0])
         if not inner:
-            return int(self._width)
+            bw = 0 if not border else 2 * self._scrollarea.get_border_size()[0]
+            return int(self._width) + bw
         return int(self._width - self._get_scrollbar_thickness()[1])
 
-    def get_height(self, inner: bool = False, widget: bool = False) -> int:
+    def get_height(self, inner: bool = False, widget: bool = False, border: bool = False) -> int:
         """
         Get the Menu height.
 
@@ -1867,15 +1869,17 @@ class Menu(Base):
 
         :param inner: If ``True`` returns the available height (menu height minus scroll and menubar)
         :param widget: If ``True`` returns the total height used by the widgets
+        :param border: If ``True`` add the menu border height. Only applied if both ``inner`` and ``widget`` are ``False``
         :return: Height in px
         """
         if widget:
             return int(self._widget_max_position[1] - self._widget_min_position[1])
         if not inner:
-            return int(self._height)
+            bh = 0 if not border else 2 * self._scrollarea.get_border_size()[1]
+            return int(self._height) + bh
         return int(self._height - self._menubar.get_height() - self._get_scrollbar_thickness()[0])
 
-    def get_size(self, inner: bool = False, widget: bool = False) -> Vector2IntType:
+    def get_size(self, inner: bool = False, widget: bool = False, border: bool = False) -> Vector2IntType:
         """
         Return the Menu size as a tuple of (width, height) in px.
 
@@ -1885,11 +1889,13 @@ class Menu(Base):
             stored in ``_current`` pointer); for such behaviour apply to
             :py:meth:`pygame_menu.menu.Menu.get_current` object.
 
-        :param inner: If ``True`` returns the available (width, height) (menu height minus scroll and menubar)
+        :param inner: If ``True`` returns the available size (width, height) (menu height minus scroll and menubar)
         :param widget: If ``True`` returns the total (width, height) used by the widgets
+        :param border: If ``True`` add the border size to the dimensions (width, height). Only applied if both ``inner`` and ``widget`` are ``False``
         :return: Tuple of (width, height) in px
         """
-        return self.get_width(inner=inner, widget=widget), self.get_height(inner=inner, widget=widget)
+        return self.get_width(inner=inner, widget=widget, border=border), \
+               self.get_height(inner=inner, widget=widget, border=border)
 
     def render(self) -> 'Menu':
         """
@@ -2454,25 +2460,25 @@ class Menu(Base):
                         continue
 
                     if event.key == ctrl.KEY_MOVE_DOWN:
-                        if self._current._down():
+                        if self._current._down(apply_sound=True):
                             self._current._last_update_mode.append(_events.MENU_LAST_MOVE_DOWN)
                             updated = True
                             break
 
                     elif event.key == ctrl.KEY_MOVE_UP:
-                        if self._current._up():
+                        if self._current._up(apply_sound=True):
                             self._current._last_update_mode.append(_events.MENU_LAST_MOVE_UP)
                             updated = True
                             break
 
                     elif event.key == ctrl.KEY_LEFT:
-                        if self._current._left():
+                        if self._current._left(apply_sound=True):
                             self._current._last_update_mode.append(_events.MENU_LAST_MOVE_LEFT)
                             updated = True
                             break
 
                     elif event.key == ctrl.KEY_RIGHT:
-                        if self._current._right():
+                        if self._current._right(apply_sound=True):
                             self._current._last_update_mode.append(_events.MENU_LAST_MOVE_RIGHT)
                             updated = True
                             break


Test patch:
diff --git a/test/test_menu.py b/test/test_menu.py
index cf76c186..06271e17 100644
--- a/test/test_menu.py
+++ b/test/test_menu.py
@@ -2417,6 +2417,37 @@ class MenuTest(BaseRSTest):
         sa.show_scrollbars(pygame_menu.locals.ORIENTATION_VERTICAL, force=False)
         self.assertEqual(sa.get_size(inner=True), (580, 400))
 
+    def test_get_size(self) -> None:
+        """
+        Test get menu size.
+        """
+        theme = pygame_menu.themes.THEME_DEFAULT.copy()
+        menu = MenuUtils.generic_menu(theme=theme)
+        self.assertEqual(menu.get_size(), (600, 400))
+
+        # Create new menu with image border
+        for scale in [(1, 1), (2, 5), (5, 2)]:
+            theme.border_color = pygame_menu.BaseImage(pygame_menu.baseimage.IMAGE_EXAMPLE_TILED_BORDER)
+            theme.border_color.scale(*scale)
+            border_size = theme.border_color.get_size()
+            menu = MenuUtils.generic_menu(theme=theme)
+            self.assertEqual(menu.get_size(), (600, 400))
+            self.assertEqual(menu.get_size(border=True),
+                             (600 + 2 * border_size[0] / 3, 400 + 2 * border_size[1] / 3))
+
+        # Create new menu with border color
+        theme.border_width = 10
+        theme.border_color = 'red'
+        menu = MenuUtils.generic_menu(theme=theme)
+        self.assertEqual(menu.get_size(border=True),
+                         (600 + 2 * theme.border_width, 400 + 2 * theme.border_width))
+
+        # Menu menu with none border color
+        theme.border_width = 10
+        theme.border_color = None
+        menu = MenuUtils.generic_menu(theme=theme)
+        self.assertEqual(menu.get_size(border=True), (600, 400))
+
     def test_border_color(self) -> None:
         """
         Test menu border color.