Repository: ppizarror/pygame-menu

Issue:
Unable to hide the cursor, due to pygame_menu
- OS: win
- python version: v3.11.1
- pygame version: v2.1.3 dev8
- pygame-menu version: v4.3.1

Due to the use of pygame_menu, pygame.mouse.set_visible() does not work


```
import sys, pygame, pygame_menu
pygame.init()

screen = pygame.display.set_mode((1920, 1080), pygame.FULLSCREEN)
pygame.mouse.set_visible(False)

menu = pygame_menu.Menu('Bug', 1920, 1080)
pygame.mouse.set_visible()

while True:
    events = pygame.event.get()
    for event in events:
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()

    key = pygame.key.get_pressed()

    if key[pygame.K_ESCAPE]:
        pygame.quit()
        sys.exit()

    menu.draw(screen)
    menu.update(events)
    pygame.display.update()
```

The cursor should have disappeared, but it still remains visible, everything works without pygame_menu



Discussion:

Patch:
diff --git a/pygame_menu/font.py b/pygame_menu/font.py
index 5378659a..2fe87dff 100644
--- a/pygame_menu/font.py
+++ b/pygame_menu/font.py
@@ -39,7 +39,7 @@ __all__ = [
 ]
 
 from pathlib import Path
-from typing import Union, Optional, Any
+from typing import Union, Optional, Any, Dict, Tuple
 import os.path as path
 
 import pygame.font as __font
@@ -71,12 +71,12 @@ FONT_EXAMPLES = (FONT_8BIT, FONT_BEBAS, FONT_COMIC_NEUE, FONT_DIGITAL, FONT_FRAN
                  FONT_PT_SERIF, FONT_FIRACODE, FONT_FIRACODE_BOLD, FONT_FIRACODE_ITALIC,
                  FONT_FIRACODE_BOLD_ITALIC)
 
-# Stores font cache
-_cache = {}
-
 FontType = Union[str, __font.Font, Path]
 FontInstance = (str, __font.Font, Path)
 
+# Stores font cache
+_cache: Dict[Tuple[FontType, int], '__font.Font'] = {}
+
 
 def assert_font(font: Any) -> None:
     """
diff --git a/pygame_menu/menu.py b/pygame_menu/menu.py
index c40a9dad..0663379d 100644
--- a/pygame_menu/menu.py
+++ b/pygame_menu/menu.py
@@ -90,7 +90,8 @@ class Menu(Base):
     :param menu_id: ID of the Menu
     :param mouse_enabled: Enable/disable mouse click inside the Menu
     :param mouse_motion_selection: Select widgets using mouse motion. If ``True`` menu draws a ``focus`` on the selected widget
-    :param mouse_visible: Set mouse visible on Menu
+    :param mouse_visible: Set mouse cursor visible on Menu. Auto-enables if the Menu has scrollbars. Also requires ``mouse_visible_update`` to be True for the Menu to update cursor visibility
+    :param mouse_visible_update: Menu can update the cursor mouse visibility. If ``False``, the Menu does not update cursor visibility
     :param onclose: Event or function executed when closing the Menu. If not ``None`` the menu disables and executes the event or function it points to. If a function (callable) is provided it can be both non-argument or single argument (Menu instance)
     :param onreset: Function executed when resetting the Menu. The function must be non-argument or single argument (Menu instance)
     :param overflow: Enables overflow on x/y axes. If ``False`` then scrollbars will not work and the maximum width/height of the scrollarea is the same as the Menu container. Style: (overflow_x, overflow_y). If ``False`` or ``True`` the value will be set on both axis
@@ -135,6 +136,7 @@ class Menu(Base):
     _mouse_motion_selection: bool
     _mouse_visible: bool
     _mouse_visible_default: bool
+    _mouse_visible_update: bool
     _mouseover: bool
     _onbeforeopen: Optional[Callable[['Menu', 'Menu'], Any]]
     _onclose: Optional[Union['_events.MenuAction', Callable[['Menu'], Any], CallableNoArgsType]]
@@ -197,6 +199,7 @@ class Menu(Base):
         mouse_enabled: bool = True,
         mouse_motion_selection: bool = False,
         mouse_visible: bool = True,
+        mouse_visible_update: bool = True,
         onclose: Optional[Union['_events.MenuAction', Callable[['Menu'], Any], CallableNoArgsType]] = None,
         onreset: Optional[Union[Callable[['Menu'], Any], CallableNoArgsType]] = None,
         overflow: Union[Vector2BoolType, bool] = (True, True),
@@ -221,6 +224,7 @@ class Menu(Base):
         assert isinstance(mouse_enabled, bool)
         assert isinstance(mouse_motion_selection, bool)
         assert isinstance(mouse_visible, bool)
+        assert isinstance(mouse_visible_update, bool)
         assert isinstance(overflow, (VectorInstance, bool))
         assert isinstance(rows, (int, type(None), VectorInstance))
         assert isinstance(theme, Theme), \
@@ -489,6 +493,7 @@ class Menu(Base):
         self._mouse_motion_selection = mouse_motion_selection
         self._mouse_visible = mouse_visible
         self._mouse_visible_default = mouse_visible
+        self._mouse_visible_update = mouse_visible_update
 
         # Init touchscreen
         if touchscreen_motion_selection:
@@ -603,21 +608,29 @@ class Menu(Base):
         width: NumberType,
         height: NumberType,
         screen_dimension: Optional[Vector2IntType] = None,
-        position: Optional[Union[Vector2NumberType, Tuple[NumberType, NumberType, bool]]] = None
+        position: Optional[Union[Vector2NumberType, Tuple[NumberType, NumberType, bool]]] = None,
+        recursive: bool = False
     ) -> 'Menu':
         """
-        Resize the menu to another width/height
+        Resizes the menu to another width/height.
 
         :param width: Menu width (px)
         :param height: Menu height (px)
         :param screen_dimension: List/Tuple representing the dimensions the Menu should reference for sizing/positioning (width, height), if ``None`` pygame is queried for the display mode. This value defines the ``window_size`` of the Menu
         :param position: Position on x-axis and y-axis. If the value is only 2 elements, the position is relative to the window width (thus, values must be 0-100%); else, the third element defines if the position is relative or not. If ``(x, y, False)`` the values of ``(x, y)`` are in px. If ``None`` use the default from the menu constructor
+        :param recursive: If true, resize all submenus in a recursive fashion
         :return: Self reference
         """
         assert isinstance(width, NumberInstance)
         assert isinstance(height, NumberInstance)
         assert width > 0 and height > 0, \
             'menu width and height must be greater than zero'
+        assert isinstance(recursive, bool)
+
+        # Resize recursively
+        if recursive:
+            for menu in self.get_submenus(True):
+                menu.resize(width, height, screen_dimension, position)
 
         # Convert to int
         width, height = int(width), int(height)
@@ -2486,7 +2499,8 @@ class Menu(Base):
         updated = False
 
         # Update mouse
-        pygame.mouse.set_visible(self._current._mouse_visible)
+        if self._mouse_visible_update:
+            pygame.mouse.set_visible(self._current._mouse_visible)
         mouse_motion_event = None
 
         selected_widget = self._current.get_selected_widget()
diff --git a/pygame_menu/version.py b/pygame_menu/version.py
index b9b93a75..892082ec 100644
--- a/pygame_menu/version.py
+++ b/pygame_menu/version.py
@@ -8,8 +8,6 @@ Library version.
 
 __all__ = ['Version', 'vernum', 'ver', 'rev']
 
-from typing import Tuple
-
 
 class Version(tuple):
     """
@@ -34,6 +32,6 @@ class Version(tuple):
     patch = property(lambda self: self[2])
 
 
-vernum = Version(4, 3, 8)
+vernum = Version(4, 3, 9)
 ver = str(vernum)
 rev = ''
diff --git a/pygame_menu/widgets/widget/colorinput.py b/pygame_menu/widgets/widget/colorinput.py
index b3f67221..82c44ec2 100644
--- a/pygame_menu/widgets/widget/colorinput.py
+++ b/pygame_menu/widgets/widget/colorinput.py
@@ -37,7 +37,7 @@ from pygame_menu.widgets.core.widget import AbstractWidgetManager, Widget
 from pygame_menu.widgets.widget.textinput import TextInput
 
 from pygame_menu._types import Union, List, NumberType, Any, Optional, CallbackType, \
-    Literal, Tuple3IntType, NumberInstance, EventVectorType, Callable
+    Tuple3IntType, NumberInstance, EventVectorType, Callable
 
 # Input modes
 COLORINPUT_TYPE_HEX = 'hex'
diff --git a/pygame_menu/widgets/widget/frame.py b/pygame_menu/widgets/widget/frame.py
index 6d8b200f..480a22bc 100644
--- a/pygame_menu/widgets/widget/frame.py
+++ b/pygame_menu/widgets/widget/frame.py
@@ -43,7 +43,7 @@ from pygame_menu.widgets.widget.button import Button
 from pygame_menu.widgets.widget.label import Label
 
 from pygame_menu._types import Optional, NumberType, Dict, Tuple, Union, List, \
-    Vector2NumberType, Literal, Tuple2IntType, NumberInstance, Any, ColorInputType, \
+    Vector2NumberType, Tuple2IntType, NumberInstance, Any, ColorInputType, \
     EventVectorType, PaddingType, CallbackType, ColorInputGradientType, \
     CursorInputType, VectorInstance
 
diff --git a/pygame_menu/widgets/widget/menubar.py b/pygame_menu/widgets/widget/menubar.py
index b6993dce..8797af0c 100644
--- a/pygame_menu/widgets/widget/menubar.py
+++ b/pygame_menu/widgets/widget/menubar.py
@@ -34,7 +34,7 @@ from pygame_menu.locals import FINGERUP, POSITION_EAST, POSITION_WEST, POSITION_
 from pygame_menu.utils import assert_color, get_finger_pos, warn
 from pygame_menu.widgets.core.widget import Widget, WidgetTransformationNotImplemented
 
-from pygame_menu._types import Tuple, CallbackType, Tuple2IntType, Literal, Any, \
+from pygame_menu._types import Tuple, CallbackType, Tuple2IntType, Any, \
     Optional, NumberInstance, ColorInputType, EventVectorType, VectorInstance, \
     List, ColorType, NumberType
 
@@ -52,7 +52,7 @@ _MODE_CLOSE = 1020
 _MODE_BACK = 1021
 
 # Custom types
-MenuBarStyleModeType = str
+MenuBarStyleModeType = int
 
 
 # noinspection PyMissingOrEmptyDocstring
diff --git a/pygame_menu/widgets/widget/selector.py b/pygame_menu/widgets/widget/selector.py
index 78155305..5f4fca3a 100644
--- a/pygame_menu/widgets/widget/selector.py
+++ b/pygame_menu/widgets/widget/selector.py
@@ -35,7 +35,7 @@ from pygame_menu.utils import check_key_pressed_valid, assert_color, assert_vect
 from pygame_menu.widgets.core.widget import Widget, AbstractWidgetManager
 
 from pygame_menu._types import Tuple, Union, List, Any, Optional, CallbackType, \
-    Literal, ColorType, ColorInputType, Tuple2IntType, Tuple3IntType, EventVectorType, \
+    ColorType, ColorInputType, Tuple2IntType, Tuple3IntType, EventVectorType, \
     Tuple2NumberType, Callable
 
 SELECTOR_STYLE_CLASSIC = 'classic'
diff --git a/pygame_menu/widgets/widget/table.py b/pygame_menu/widgets/widget/table.py
index 976e04e1..cb751e15 100644
--- a/pygame_menu/widgets/widget/table.py
+++ b/pygame_menu/widgets/widget/table.py
@@ -265,9 +265,9 @@ class Table(Frame):
 
         # If cells is a previous table row
         if isinstance(cells, Frame) and cells.has_attribute('is_row'):
-            row_cells = list(cells.get_widgets(unpack_subframes=False))
+            _row_cells = list(cells.get_widgets(unpack_subframes=False))
             cells.clear()
-            cells = row_cells
+            cells = _row_cells
         if isinstance(cells, Widget):
             cells = [cells]
 


Test patch:
diff --git a/test/_utils.py b/test/_utils.py
index 72b0350e..46b88295 100644
--- a/test/_utils.py
+++ b/test/_utils.py
@@ -21,11 +21,9 @@ __all__ = [
     'reset_widgets_over',
     'sleep',
     'surface',
-    'test_reset_surface',
 
     # Class utils
     'BaseTest',
-    'BaseRSTest',
     'PygameEventUtils',
     'MenuUtils'
 
@@ -94,18 +92,18 @@ class BaseTest(unittest.TestCase):
     Base test class.
     """
 
-
-class BaseRSTest(unittest.TestCase):
-    """
-    Test class that Reset the Surface (RS) each time a test runs.
-    """
-
     def setUp(self) -> None:
         """
         Reset the surface.
         """
         test_reset_surface()
 
+    def tearDown(self) -> None:
+        """
+        Reset the surface.
+        """
+        test_reset_surface()
+
 
 class PygameEventUtils(object):
     """
diff --git a/test/test_examples.py b/test/test_examples.py
index 53952eac..6834394d 100644
--- a/test/test_examples.py
+++ b/test/test_examples.py
@@ -8,7 +8,7 @@ Test example files.
 
 __all__ = ['ExamplesTest']
 
-from test._utils import BaseRSTest, MenuUtils, PygameEventUtils, test_reset_surface
+from test._utils import BaseTest, MenuUtils, PygameEventUtils
 
 import pygame
 import pygame_menu
@@ -30,11 +30,8 @@ import pygame_menu.examples.other.scrollbar_area as scrollbar_area
 import pygame_menu.examples.other.ui_solar_system as ui_solarsystem
 import pygame_menu.examples.other.widget_positioning as widget_positioning
 
-# Reset the surface as some example could have changed it
-test_reset_surface()
 
-
-class ExamplesTest(BaseRSTest):
+class ExamplesTest(BaseTest):
 
     def test_example_game_selector(self) -> None:
         """
diff --git a/test/test_menu.py b/test/test_menu.py
index 4a390646..e95f83f8 100644
--- a/test/test_menu.py
+++ b/test/test_menu.py
@@ -8,7 +8,7 @@ Menu object tests.
 
 __all__ = ['MenuTest']
 
-from test._utils import BaseRSTest, surface, MenuUtils, PygameEventUtils, \
+from test._utils import BaseTest, surface, MenuUtils, PygameEventUtils, \
     TEST_THEME, PYGAME_V2, WIDGET_MOUSEOVER, WIDGET_TOP_CURSOR, reset_widgets_over, \
     THEME_NON_FIXED_TITLE
 import copy
@@ -40,7 +40,7 @@ def dummy_function() -> None:
     return
 
 
-class MenuTest(BaseRSTest):
+class MenuTest(BaseTest):
 
     def test_mainloop_disabled(self) -> None:
         """
@@ -2378,7 +2378,7 @@ class MenuTest(BaseRSTest):
         self.assertEqual(menu._column_max_width, [300])
         self.assertEqual(menu._menubar._width, 300)
 
-        # render
+        # Render
         self.assertIsNone(menu._widgets_surface)
         menu.render()
         self.assertIsNotNone(menu._widgets_surface)
@@ -2435,6 +2435,19 @@ class MenuTest(BaseRSTest):
         sa.show_scrollbars(pygame_menu.locals.ORIENTATION_VERTICAL, force=False)
         self.assertEqual(sa.get_size(inner=True), (580, 400))
 
+        # Test submenu recursive resizing
+        menu = MenuUtils.generic_menu(theme=theme)
+        menu2 = MenuUtils.generic_menu(theme=theme)
+        menu3 = MenuUtils.generic_menu(theme=theme)
+        menu.add.button('btn', menu2)
+        menu2.add.button('btn', menu3)
+        self.assertEqual(menu.get_submenus(True), (menu2, menu3))
+        for m in (menu, menu2, menu3):
+            self.assertEqual(m.get_size(), (600, 400))
+        menu.resize(300, 300, recursive=True)  # Now, resize
+        for m in (menu, menu2, menu3):
+            self.assertEqual(m.get_size(), (300, 300))
+
     def test_get_size(self) -> None:
         """
         Test get menu size.
diff --git a/test/test_scrollarea.py b/test/test_scrollarea.py
index acf22bf1..30eda782 100644
--- a/test/test_scrollarea.py
+++ b/test/test_scrollarea.py
@@ -150,6 +150,10 @@ class ScrollAreaTest(BaseTest):
         s1.hide()
         self.assertFalse(s1.is_visible())
 
+        # Check scrollbar render
+        s1._slider_rect = None
+        self.assertIsNone(s1._render())
+
     def test_size(self) -> None:
         """
         Test size.
diff --git a/test/test_widget_frame.py b/test/test_widget_frame.py
index e7ea673f..f6921b1d 100644
--- a/test/test_widget_frame.py
+++ b/test/test_widget_frame.py
@@ -9,9 +9,8 @@ its layout and contains other widgets.
 
 __all__ = ['FrameWidgetTest']
 
-from test._utils import MenuUtils, surface, PygameEventUtils, test_reset_surface, \
+from test._utils import BaseTest, MenuUtils, surface, PygameEventUtils, \
     TEST_THEME, PYGAME_V2, WIDGET_MOUSEOVER, reset_widgets_over, THEME_NON_FIXED_TITLE
-import unittest
 
 import pygame
 import pygame_menu
@@ -30,13 +29,7 @@ from pygame_menu._scrollarea import get_scrollbars_from_position
 from pygame_menu.widgets.widget.frame import _FrameDoNotAcceptScrollarea
 
 
-class FrameWidgetTest(unittest.TestCase):
-
-    def setUp(self) -> None:
-        """
-        Setup frame widget test.
-        """
-        test_reset_surface()
+class FrameWidgetTest(BaseTest):
 
     def test_general(self) -> None:
         """