Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reducing the selection_box_height for more than one dropselect during runtime fails #491

Open
gabindu opened this issue Jan 17, 2025 · 4 comments
Labels

Comments

@gabindu
Copy link

gabindu commented Jan 17, 2025

Environment information

  • OS: Mac OS 15.2, Windows 11
  • python version: v3.11.11 and 3.12.4
  • pygame version: v2.6.1
  • pygame-menu version: v4.5.1

Describe the bug

Reducing the selection_box_height for more than one dropselect_multi during runtime (for example when the game window was resized) fails, since the second dropselect still has the old size (which can be larger than the new menu size) - but is forced to render while trying to update the first one.

My understanding is that calling _make_selection_drop() causes a full rerender of the full menu - and therefore the second droplist, which hasn't been adjusted yet.

I have attempted to set all selection_box_height values for all dropselects first, before then calling _make_selection_drop(), but this has also failed.

It's possible that I'm just not aware of another API function which would help with this, if so, please let me know.

To Reproduce

This is maybe not as minimal as it could be, but shows:

  • that resizing works fine when there is only one dropselect
  • resizing fails when there are two dropselects
  • resizing also fails when the second dropselect is hidden
  • completely removing the second dropselect (and any reference to it) returns to the good behaviour.
import pygame
import pygame_menu
import os

# --------------------------------------------------------------------------------
# create a resizable pygame window
# --------------------------------------------------------------------------------
# ensure window is at 0,0 for easier testing
os.environ["SDL_VIDEO_WINDOW_POS"] = "0,0"  # has to be called before pygame.init()

pygame.init()

w, h = (800, 600)
gamewindow = pygame.display.set_mode((w, h), pygame.RESIZABLE)

# --------------------------------------------------------------------------------
# setup the menu
# --------------------------------------------------------------------------------

my_menu_theme = pygame_menu.themes.THEME_DARK.copy()
my_menu_theme.background_color = pygame_menu.BaseImage(
    image_path=pygame_menu.baseimage.IMAGE_EXAMPLE_GRAY_LINES,
    drawing_mode=pygame_menu.baseimage.IMAGE_MODE_REPEAT_XY,
)

ftsize = 24
my_menu_theme.widget_font_size = ftsize


def menusize():
    """return a size that is 95% of the current window size"""
    w, h = gamewindow.get_size()
    return (int(w * 0.95), int(h * 0.95))


menu = pygame_menu.Menu(
    width=menusize()[0],
    height=menusize()[1],
    theme=my_menu_theme,
    title="Resizing test, ok with 1 dropselect",
)


# function to estimate the max number of items we can show without scrolling
def estimate_max_items():
    return int((menu.get_height() - 55) / (ftsize * 1.5)) - 2


max_items = estimate_max_items()

# add some debugging information
l1 = menu.add.label(f"Current window size: {w}x{h}")
l2 = menu.add.label(f"Current menu size: {menu.get_width()}x{menu.get_height()}")
l3 = menu.add.label(f"Current menu title height: {menu.get_menubar().get_height()}")
l4 = menu.add.label(f"Current selection box height: {max_items}")

menu.add.vertical_margin(20)

# --------------------------------------------------------------------------------
# a dropselect with many entries
# --------------------------------------------------------------------------------
droplist1 = menu.add.dropselect_multiple(
    title="droplist1",
    dropselect_multiple_id="droplist1",
    placeholder_add_to_selection_box=False,
    items=[(f"{a:02}", a) for a in range(1, 100)],
    default=[2, 5, 7],
    #    dropselect_multiple_id="droplist1",
    open_middle=True,  # if false, the checkboxes are drawn incorrectly?
    selection_box_height=max_items,
    selection_infinite=True,
)
droplist1.reset_value()  # needed to update the highlights before entering?


# --------------------------------------------------------------------------------
# Prepare to add a second dropselect later
# --------------------------------------------------------------------------------

droplist2 = None
hidedroplist2 = False


def add_droplist2():
    global droplist2

    menu.set_title("Resizing test, crash with 2 dropselect")

    max_items = estimate_max_items()

    menu.remove_widget(lastbutton)

    droplist2 = menu.add.dropselect_multiple(
        title="droplist2",
        dropselect_multiple_id="droplist2",
        placeholder_add_to_selection_box=False,
        items=[(f"{a:02}", a) for a in range(1, 100)],
        default=[2, 5, 7],
        #        dropselect_multiple_id="droplist2",
        open_middle=True,  # if false, the checkboxes are drawn incorrectly?
        selection_box_height=max_items,
        selection_infinite=True,
    )
    #    droplist2.reset_value()  # needed to update the highlights before entering?

    menu.add.vertical_margin(20)

    def toggle_droplist2(data):
        global hidedroplist2

        hidedroplist2 = data
        if data:
            droplist2.hide()
        else:
            droplist2.show()

    def remove_droplist2():
        global droplist2

        # remove droplist2 (and the hide/remove buttons)
        for w in [droplist2, hide_button, remove_button]:
            menu.remove_widget(w)

        droplist2 = None  # also remove the reference to the widget

        menu.force_surface_update()
        menu.render()

    hide_button = menu.add.toggle_switch(
        "Hide droplist2",
        default=False,
        onchange=toggle_droplist2,
    )
    remove_button = menu.add.button("Remove droplist2", remove_droplist2)

    menu.add.vertical_margin(20)
    menu.add.button("Quit", pygame_menu.events.EXIT)


menu.add.vertical_margin(60)
lastbutton = menu.add.button("Add droplist2", add_droplist2)

# --------------------------------------------------------------------------------
# Function to resize and readjust the max number of items in the dropselect
# (gets called when the window is resized)
# --------------------------------------------------------------------------------


def refresh_menu():
    w, h = gamewindow.get_size()
    menu.resize(*menusize())

    # estimate the max number of items we can show without scrolling
    max_items = estimate_max_items()

    widgetnames = ["droplist1"]
    if droplist2 and not hidedroplist2:
        widgetnames.append("droplist2")

    # go through the dropselects and update the selection box height
    for widgetname in widgetnames:
        ds = menu.get_widget(widgetname)
        print(f"**** Updating widget {widgetname} ****")

        print(
            f"(BEFORE) drop_frame_total_height = {ds._drop_frame.get_attribute('height')}"
        )

        # update the selection box height
        ds._selection_box_height = max_items
        # force the dropselect to recreate the selection box
        ds._make_selection_drop()
        print(
            f"(AFTER)  drop_frame_total_height = {ds._drop_frame.get_attribute('height')}"
        )

        # update debug info labels
        l1.set_title(f"Current window size: {w}x{h}")
        l2.set_title(f"Current menu size: {menu.get_width()}x{menu.get_height()}")
        l3.set_title(f"Current menu title height: {menu.get_menubar().get_height()}")
        l4.set_title(f"Current selection box height: {max_items}")

    menu.render()


# --------------------------------------------------------------------------------


if __name__ == "__main__":

    while menu.is_enabled():

        events = pygame.event.get()
        for event in events:
            if event.type == pygame.QUIT or (
                event.type == pygame.KEYDOWN and (event.key == pygame.K_q)
            ):
                menu.disable()

            elif event.type == pygame.VIDEORESIZE:
                # make sure the previously drawn menu (of different size) is cleared
                gamewindow.fill((0, 0, 0))
                # now resize the menu and adjust the dropselects
                refresh_menu()

        if menu.is_enabled():
            menu.draw(gamewindow)
            menu.update(events)

            pygame.display.update()


# --------------------------------------------------------------------------------

Expected behavior

It should be possible to resize several dropselect selection boxes simultaneously, before the menu gets rendered.

(Additionally, it would be nice if there was an easier way to know beforehand what the largest possible selection_box_height can be for a given menu size. Maybe even allowing this to be done automatically, maybe by setting selection_box_height=0?)

@gabindu gabindu added the bug label Jan 17, 2025
@gabindu
Copy link
Author

gabindu commented Jan 17, 2025

I should have added instructions on how to use the example code to reproduce the problem, sorry:

  • Start the program, then resize the window as wanted (larger or smaller). The selection_box_height of the single droplist gets correctly automatically updated, to always show the largest possible number of items.
  • Click on "add droplist2". Now repeat resizing the window:
    • Reducing the window size only by a small amount should work fine.
    • But reducing the window height more drastically will cause an AssertionError: selection box height (862) cannot be greater than menu height (680)
  • Now restart, "add droplist2", then "hide droplist2": Resizing in this case will also fail (assuming one reduces the height by enough) - which I guess is not surprising, since the droplist still exists (even though it is hidden).
  • As a final test, restart, "add droplist2", then "remove droplist2": This removes the widget from the menu, and also removes all references to the widget object. In this case, resizing again works correctly, just as if one had only one dropselect to begin with.

@gabindu
Copy link
Author

gabindu commented Jan 17, 2025

Thanks for looking into this, and for creating this awesome package in the first place!

@gabindu
Copy link
Author

gabindu commented Jan 17, 2025

A separate issue related to this: Is it normal that a call to reset_value() is needed before the default values are shown as selected in the dropbox_multi before entering it? In my example code, I used this for droplist1, but not for droplist2, to show the difference.

@ppizarror
Copy link
Owner

Hi @gabindu thanks for the detailed explanation. I will look at this this weekend and let you know when the fix is ready and uploaded to pip.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants