The code for minesweeper is divided into 4 files: The main code (which glues everything together), the game logic, and a couple of custom UI elements. In many games, I try and separate the game logic from the user interface, which makes porting to other platforms much easier.
Main Code
The main code.py file handles setting everything up, responding to the user inputs, and updating the User Interface. Everything within the UI is handled by CircuitPython'sĀ displayio. There are some custom controls such as dialogs made using the TextBoxĀ component of the adafruit_display_text library as well as an event button and menu control, which will be looked at in more detail below. This file is fairly self explanatory with comments throughout the file.
The setup mainly consists of setting up the game board, setting up the menus and dialogs, and setting up the mine count and time labels.
# SPDX-FileCopyrightText: 2025 Melissa LeBlanc-Williams for Adafruit Industries
# SPDX-License-Identifier: MIT
"""
An implementation of minesweeper. The logic game where the player
correctly identifies the locations of mines on a grid by clicking on squares
and revealing the number of mines in adjacent squares.
The player can also flag squares they suspect contain mines. The game ends when
the player successfully reveals all squares without mines or clicks on a mine.
"""
import array
from displayio import Group, OnDiskBitmap, TileGrid, Bitmap, Palette
from adafruit_display_text.bitmap_label import Label
from adafruit_display_text.text_box import TextBox
from eventbutton import EventButton
import supervisor
import terminalio
from adafruit_usb_host_mouse import find_and_init_boot_mouse
from gamelogic import GameLogic, BLANK, INFO_BAR_HEIGHT, DIFFICULTIES
from menu import Menu, SubMenu
# pylint: disable=ungrouped-imports
if hasattr(supervisor.runtime, "display") and supervisor.runtime.display is not None:
# use the built-in HSTX display for Metro RP2350
display = supervisor.runtime.display
else:
# pylint: disable=ungrouped-imports
from displayio import release_displays
import picodvi
import board
import framebufferio
# initialize display
release_displays()
fb = picodvi.Framebuffer(
320,
240,
clk_dp=board.CKP,
clk_dn=board.CKN,
red_dp=board.D0P,
red_dn=board.D0N,
green_dp=board.D1P,
green_dn=board.D1N,
blue_dp=board.D2P,
blue_dn=board.D2N,
color_depth=16,
)
display = framebufferio.FramebufferDisplay(fb)
supervisor.runtime.display = display
game_logic = GameLogic(display) # pylint: disable=no-value-for-parameter
# Load the spritesheet
sprite_sheet = OnDiskBitmap("bitmaps/game_sprites.bmp")
# Main group will hold all the visual layers
main_group = Group()
display.root_group = main_group
# Add Background to the Main Group
background = Bitmap(display.width, display.height, 1)
bg_color = Palette(1)
bg_color[0] = 0xaaaaaa
main_group.append(TileGrid(
background,
pixel_shader=bg_color
))
# Add Game group, which holds the game board, to the main group
game_group = Group()
main_group.append(game_group)
# Add a group for the UI Elements
ui_group = Group()
main_group.append(ui_group)
MENU_ITEM_HEIGHT = INFO_BAR_HEIGHT
def create_game_board():
# Remove the old game board
if len(game_group) > 0:
game_group.pop()
x = display.width // 2 - (game_logic.grid_width * 16) // 2
y = ((display.height - INFO_BAR_HEIGHT) // 2 -
(game_logic.grid_height * 16) // 2 + INFO_BAR_HEIGHT)
# Create a new game board
game_board = TileGrid(
sprite_sheet,
pixel_shader=sprite_sheet.pixel_shader,
width=game_logic.grid_width,
height=game_logic.grid_height,
tile_height=16,
tile_width=16,
x=x,
y=y,
default_tile=BLANK,
)
game_group.append(game_board)
return game_board
def update_ui():
# Update the UI elements with the current game state
mines_left_label.text = f"Mines: {game_logic.mines_left}"
elapsed_time_label.text = f"Time: {game_logic.elapsed_time}"
# variable for the mouse USB device instance
mouse = find_and_init_boot_mouse(cursor_image="bitmaps/mouse_cursor.bmp")
if mouse is None:
raise RuntimeError("No mouse found. Please connect a USB mouse.")
mouse.sensitivity = 4 # Slow the mouse down a bit
mouse_tg = mouse.tilegrid
mouse_tg.x = display.width // 2
mouse_tg.y = display.height // 2
# add mouse graphic to main_group
main_group.append(mouse_tg)
buf = array.array("b", [0] * 4)
waiting_for_release = False
chord_selected = False
left_button = right_button = False
mouse_coords = (display.width // 2, display.height // 2)
# Create the UI Elements (Ideally fit into 320x16 area)
# Label for the Mines Left (Left of Center)
mines_left_label = Label(
terminalio.FONT,
color=0x000000,
x=5,
y=0,
)
mines_left_label.anchor_point = (0, 0)
mines_left_label.anchored_position = (5, 2)
ui_group.append(mines_left_label)
# Label for the Elapsed Time (Right of Center)
elapsed_time_label = Label(
terminalio.FONT,
color=0x000000,
x=display.width - 50,
y=0,
)
elapsed_time_label.anchor_point = (1, 0)
elapsed_time_label.anchored_position = (display.width - 5, 2)
ui_group.append(elapsed_time_label)
# Menu button to change difficulty
difficulty_menu = SubMenu(
"Difficulty",
70,
80,
display.width // 2 - 70,
0
)
reset_menu = SubMenu(
"Reset",
50,
40,
display.width // 2 + 15,
0
)
message_dialog = Group()
message_dialog.hidden = True
def reset():
# Reset the game logic
game_logic.reset()
# Create a new game board and assign it into the game logic
game_logic.game_board = create_game_board()
message_dialog.hidden = True
def set_difficulty(diff):
game_logic.difficulty = diff
reset()
difficulty_menu.select_item(DIFFICULTIES[diff]['label'].lower().replace(" ", "_"))
def hide_group(group):
group.hidden = True
for i, difficulty in enumerate(DIFFICULTIES):
# Create a button for each difficulty
selected = i == game_logic.difficulty
difficulty_menu.add_item((set_difficulty, i), difficulty['label'], selected)
reset_menu.add_item(reset, "OK")
menu = Menu()
menu.append(difficulty_menu)
menu.append(reset_menu)
ui_group.append(menu)
reset()
message_label = TextBox(
terminalio.FONT,
text="",
color=0x333333,
background_color=0xEEEEEE,
width=display.width // 4,
height=50,
align=TextBox.ALIGN_CENTER,
padding_top=5,
)
message_label.anchor_point = (0, 0)
message_label.anchored_position = (
display.width // 2 - message_label.width // 2,
display.height // 2 - message_label.height // 2,
)
message_dialog.append(message_label)
message_button = EventButton(
(hide_group, message_dialog),
label="OK",
width=40,
height=16,
x=display.width // 2 - 20,
y=display.height // 2 - message_label.height // 2 + 20,
style=EventButton.RECT,
)
message_dialog.append(message_button)
ui_group.append(message_dialog)
# Popup message for game over/win
menus = (reset_menu, difficulty_menu)
# main loop
while True:
update_ui()
# update cursor position, and check for clicks
mouse.update()
buttons = mouse.pressed_btns
# Extract button states
if buttons is None or buttons == ():
left_button = 0
right_button = 0
else:
left_button = 1 if 'left' in buttons else 0
right_button = 1 if 'right' in buttons else 0
mouse_coords = (mouse_tg.x, mouse_tg.y)
if waiting_for_release and not left_button and not right_button:
# If both buttons are released, we can process the next click
waiting_for_release = False
if not message_dialog.hidden:
if message_button.handle_mouse(mouse_coords, left_button, waiting_for_release):
waiting_for_release = True
continue
if menu.handle_mouse(mouse_coords, left_button, waiting_for_release):
waiting_for_release = True
else:
# process gameboard click if no menu
ms_board = game_logic.game_board
if left_button and right_button and not chord_selected:
chord_coords = ((mouse_tg.x - ms_board.x) // 16, (mouse_tg.y - ms_board.y) // 16)
chord_selected = game_logic.square_chord_highlight(chord_coords)
if chord_selected:
waiting_for_release = True
if (ms_board.x <= mouse_tg.x <= ms_board.x + game_logic.grid_width * 16 and
ms_board.y <= mouse_tg.y <= ms_board.y + game_logic.grid_height * 16 and
not waiting_for_release):
coords = ((mouse_tg.x - ms_board.x) // 16, (mouse_tg.y - ms_board.y) // 16)
if chord_selected:
chord_selected = False
game_logic.square_chord_highlight(chord_coords, False)
game_logic.square_chorded(chord_coords)
elif right_button:
game_logic.square_flagged(coords)
elif left_button:
if not game_logic.square_clicked(coords):
message_label.text = "Game Over"
message_dialog.hidden = False
if left_button or right_button:
waiting_for_release = True
status = game_logic.check_for_win()
if status:
message_label.text = "You win!"
message_dialog.hidden = False
# Display message
if status is None:
continue
Game Logic
The game logic handles the Minesweeper game logic. Much of the logic was borrowed from theĀ CircuitPython Minesweeper Game guide, which is a basic version of Minesweeper designed for the PyPortal with touch input. This version adds difficulty levels as well as a timer.
This file starts by defining the difficulty levels. Each difficulty level has a label (which automatically appears in the Difficulty menu), the size of the grid, and the number of mines. Feel free to add your own additional levels, but keep in mind, the maximum grid size is limited by the screen size to 20x14. The mine count will need to be at least 10 and no more than grid_width - 1 * grid_height - 1, which effective makes 4x5 the smallest possible grid size.
DIFFICULTIES = (
{
'label': "Beginner",
'grid_size': (8,8),
'mines': 10,
},
{
'label': "Intermediate",
'grid_size': (14, 14),
'mines': 30,
},
{
'label': "Expert",
'grid_size': (20, 14),
'mines': 58,
},
)
The choices are automatically added to the Difficulty menu in the order in which they appear in the DIFFICULTIES list. Also, having more than about 14 level choices may cause the menu to be longer than the screen height, which would make the choice unavailable. Here are the default difficulty settings.
# SPDX-FileCopyrightText: 2025 Melissa LeBlanc-Williams for Adafruit Industries
# SPDX-License-Identifier: MIT
import random
from microcontroller import nvm
from adafruit_ticks import ticks_ms
from displayio import TileGrid
# Mine Densities are about the same as the original
DIFFICULTIES = (
{
'label': "Beginner",
'grid_size': (8,8),
'mines': 10,
},
{
'label': "Intermediate",
'grid_size': (14, 14),
'mines': 30,
},
{
'label': "Expert",
'grid_size': (20, 14),
'mines': 58,
},
)
INFO_BAR_HEIGHT = 16
OPEN = 0
OPEN1 = 1
OPEN2 = 2
OPEN3 = 3
OPEN4 = 4
OPEN5 = 5
OPEN6 = 6
OPEN7 = 7
OPEN8 = 8
BLANK = 9
FLAG = 10
MINE_CLICKED = 11
MINE_FLAGGED_WRONG = 12
MINE = 13
MINE_QUESTION = 14
MINE_QUESTION_OPEN = 15
STATUS_NEWGAME = 0
STATUS_PLAYING = 1
STATUS_WON = 2
STATUS_LOST = 3
class GameLogic:
def __init__(self, display):
self._board_data = bytearray()
self.game_board = None
self._difficulty = nvm[0]
if self._difficulty not in DIFFICULTIES:
self._difficulty = 0
self._display = display
self._start_time = None
self._end_time = None
self._mine_count = 0
self._status = STATUS_NEWGAME
self.reset()
def reset(self):
if (self.grid_width * 16 > self._display.width or
self.grid_height * 16 > self._display.height - INFO_BAR_HEIGHT):
raise ValueError("Grid size exceeds display size")
self._mine_count = DIFFICULTIES[self._difficulty]['mines']
if self._mine_count > (self.grid_width - 1) * (self.grid_height - 1):
raise ValueError("Too many mines for grid size")
if self._mine_count < 10:
raise ValueError("There must be at least 10 mines")
self._board_data = bytearray(self.grid_width * self.grid_height)
self._status = STATUS_NEWGAME
self._start_time = None
self._end_time = None
def _seed_mines(self, coords):
for _ in range(DIFFICULTIES[self._difficulty]['mines']):
while True:
mine_x = random.randint(0, self.grid_width - 1)
mine_y = random.randint(0, self.grid_height - 1)
if self._get_data(mine_x, mine_y) == 0 and (mine_x, mine_y) != coords:
self._set_data(mine_x, mine_y, MINE)
break
self._compute_counts()
def _set_data(self, x, y, value):
self._board_data[y * self.grid_width + x] = value
def _get_data(self, x, y):
if x < 0 or x >= self.grid_width or y < 0 or y >= self.grid_height:
return None # out of bounds, do nothing
return self._board_data[y * self.grid_width + x]
def _set_board(self, x, y, value):
if not isinstance(self.game_board, TileGrid):
raise ValueError("Game board not initialized")
self.game_board[x, y] = value # pylint: disable=unsupported-assignment-operation
def _get_board(self, x, y):
if not isinstance(self.game_board, TileGrid):
raise ValueError("Game board not initialized")
if x < 0 or x >= self.grid_width or y < 0 or y >= self.grid_height:
return None # out of bounds, do nothing
return self.game_board[x, y] # pylint: disable=unsubscriptable-object
def _compute_counts(self):
"""For each mine, increment the count in each non-mine square around it"""
for y in range(self.grid_height):
for x in range(self.grid_width):
if self._get_data(x, y) != MINE:
continue # keep looking for mines
for dx in (-1, 0, 1):
if x + dx < 0 or x + dx >= self.grid_width:
continue # off screen
for dy in (-1, 0, 1):
if y + dy < 0 or y + dy >= self.grid_height:
continue # off screen
grid_value = self._get_data(x + dx, y + dy)
if grid_value == MINE:
continue # don't process mines
self._set_data(x + dx, y + dy, grid_value + 1)
def _flag_count(self):
flags = 0
for x in range(self.grid_width):
for y in range(self.grid_height):
if self._get_board(x, y) == FLAG:
flags += 1
return flags
def expand_uncovered(self, start_x, start_y):
# pylint: disable=too-many-nested-blocks
number_uncovered = 1
stack = [(start_x, start_y)]
while len(stack) > 0:
x, y = stack.pop()
if self._get_board(x, y) == BLANK:
under_the_tile = self._get_data(x, y)
if under_the_tile <= OPEN8:
self._set_board(x, y, under_the_tile)
number_uncovered += 1
if under_the_tile == OPEN:
for dx in (-1, 0, 1):
if x + dx < 0 or x + dx >= self.grid_width:
continue # off screen
for dy in (-1, 0, 1):
if y + dy < 0 or y + dy >= self.grid_height:
continue # off screen
if dx == 0 and dy == 0:
continue # don't process where the mine
stack.append((x + dx, y + dy))
return number_uncovered
def square_flagged(self, coords):
if self._status in (STATUS_WON, STATUS_LOST):
return False
x, y = coords
TOGGLE_STATES = (BLANK, FLAG, MINE_QUESTION)
for state in TOGGLE_STATES:
if self._get_board(x, y) == state:
self._set_board(x, y,
TOGGLE_STATES[(TOGGLE_STATES.index(state) + 1) % len(TOGGLE_STATES)])
break
return True
def square_chorded(self, coords):
if self._status in (STATUS_WON, STATUS_LOST):
return False
x, y = coords
if x < 0 or x >= self.grid_width or y < 0 or y >= self.grid_height:
return True # out of bounds, do nothing
value = self._get_board(x, y)
if value not in (OPEN1, OPEN2, OPEN3, OPEN4, OPEN5, OPEN6, OPEN7, OPEN8):
return True # Nothing to do if not an open numbered square
# Pre-compute valid neighbors
neighbors = [
(nx, ny)
for nx in range(x - 1, x + 2)
for ny in range(y - 1, y + 2)
if (0 <= nx < self.grid_width
and 0 <= ny < self.grid_height
and not (nx == x and ny == y))
]
# Count flagged neighbors
flags = sum(1 for nx, ny in neighbors if self._get_board(nx, ny) == FLAG)
if flags != value:
return True # not enough flags, do nothing
# Uncover all non-flagged neighbors
for nx, ny in neighbors:
if self._get_board(nx, ny) != FLAG:
if not self.square_clicked((nx, ny)):
return False # lost
return True
def square_chord_highlight(self, coords, highlight=True):
if self._status in (STATUS_WON, STATUS_LOST):
return False
x, y = coords
if x < 0 or x >= self.grid_width or y < 0 or y >= self.grid_height:
return False # out of bounds, do nothing
value = self._get_board(x, y)
if value not in (OPEN1, OPEN2, OPEN3, OPEN4, OPEN5, OPEN6, OPEN7, OPEN8):
return False # Nothing to do if not an open numbered square
# Pre-compute valid neighbors
neighbors = [
(nx, ny)
for nx in range(x - 1, x + 2)
for ny in range(y - 1, y + 2)
if (0 <= nx < self.grid_width
and 0 <= ny < self.grid_height
and not (nx == x and ny == y))
]
# Highlight all non-flagged squares around here
for nx, ny in neighbors:
if highlight:
if self._get_board(nx, ny) == BLANK:
self._set_board(nx, ny,MINE_QUESTION_OPEN)
else:
if self._get_board(nx, ny) == MINE_QUESTION_OPEN:
self._set_board(nx, ny, BLANK)
return True
def square_clicked(self, coords):
x, y = coords
if self._status in (STATUS_WON, STATUS_LOST):
return False
# First click is never a mine, so start the game
if self._status == STATUS_NEWGAME:
self._seed_mines(coords)
self._status = STATUS_PLAYING
if self._start_time is None:
self._start_time = ticks_ms()
if self._get_board(x, y) not in (FLAG, None):
under_the_tile = self._get_data(x, y)
if under_the_tile == MINE:
self._set_data(x, y, MINE_CLICKED)
self._set_board(x, y, MINE_CLICKED)
self._status = STATUS_LOST
self.reveal_board()
if self._end_time is None:
self._end_time = ticks_ms()
return False #lost
elif OPEN1 <= under_the_tile <= OPEN8:
self._set_board(x, y, under_the_tile)
elif under_the_tile == OPEN:
self._set_board(x, y, BLANK)
self.expand_uncovered(x, y)
else:
raise ValueError(f'Unexpected value {under_the_tile} on board')
return True
def reveal_board(self):
for x in range(self.grid_width):
for y in range(self.grid_height):
if self._get_board(x, y) == FLAG and self._get_data(x, y) != MINE:
self._set_board(x, y, MINE_FLAGGED_WRONG)
else:
self._set_board(x, y, self._get_data(x, y))
def check_for_win(self):
"""Check for a complete, winning game. That's one with all squares uncovered
and all bombs correctly flagged, with no non-bomb squares flaged.
"""
if self._status in (STATUS_WON, STATUS_LOST):
return None
# first make sure everything has been explored and decided
for x in range(self.grid_width):
for y in range(self.grid_height):
if self._get_board(x, y) == BLANK or \
self._get_board(x, y) == MINE_QUESTION or \
self._get_board(x, y) == MINE_QUESTION_OPEN:
return None # still ignored or question squares
# then check for mistagged bombs
for x in range(self.grid_width):
for y in range(self.grid_height):
if self._get_board(x, y) == FLAG and self._get_data(x, y) != MINE:
return False # misflagged bombs, not done
self._status = STATUS_WON
if self._end_time is None:
self._end_time = ticks_ms()
return True # nothing unexplored, and no misflagged bombs
@property
def grid_width(self):
return DIFFICULTIES[self._difficulty]['grid_size'][0]
@property
def grid_height(self):
return DIFFICULTIES[self._difficulty]['grid_size'][1]
@property
def status(self):
return self._status
@property
def elapsed_time(self):
"""Elapsed time in seconds since the game started with a maximum of 999 seconds"""
if self._start_time is None:
return 0
if self._end_time is None:
return min(999, (ticks_ms() - self._start_time) // 1000)
return min(999, (self._end_time - self._start_time) // 1000)
@property
def mines_left(self):
# This number can be negative
return self._mine_count - self._flag_count()
@property
def difficulty(self):
return self._difficulty
@difficulty.setter
def difficulty(self, value):
if not 0 <= value < len(DIFFICULTIES):
raise ValueError("Invalid difficulty option")
self._difficulty = value
nvm[0] = value
self.reset()
Event Button
The event button builds on the standard button available in the adafruit_button library. It adds the option to specify a callback function when the button is clicked as well as some mouse handling code so that a click is only registered if another element wasn't already selected and the click is within the boundaries of the button. This way if another UI element is selected and the mouse is dragged onto the button, it is handled properly.
# SPDX-FileCopyrightText: 2025 Melissa LeBlanc-Williams for Adafruit Industries
# SPDX-License-Identifier: MIT
from adafruit_button import Button
class EventButton(Button):
"""A button that can be used to trigger a callback when clicked.
:param callback: The callback function to call when the button is clicked.
A tuple can be passed with an argument that will be passed to the
callback function. The first element of the tuple should be the
callback function, and the remaining elements will be passed as
arguments to the callback function.
"""
def __init__(self, callback, *args, **kwargs):
super().__init__(*args, **kwargs)
self.args = []
if isinstance(callback, tuple):
self.callback = callback[0]
self.args = callback[1:]
else:
self.callback = callback
def click(self):
"""Call the function when the button is pressed."""
self.callback(*self.args)
def handle_mouse(self, point, clicked, waiting_for_release):
# pylint: disable=attribute-defined-outside-init
if waiting_for_release:
return False
# Handle mouse events for the button
if self.contains(point):
self.selected = True
if clicked:
self.click()
return True
else:
self.selected = False
return False
Menu
The menu is used for handling the options at the top. This allows selecting the difficulty or resetting the game. The reason that the reset option is inside a menu is to help avoid accidentally clicking the button when playing with some of the larger game grids. It was cleaner to implement this way than a dialog that popped up asking to confirm.
The menu class is really just a container for the submenus with similar mouse handling code to that of the event button. When one of the menu items is clicked (which is really just an event button), the submenu's contents are drawn below it, which also consists of event buttons. Each submenu is in a separate displayio to help with controlling the visibility.
Submenu items can also be "selected", which highlights the selected item in green. This is useful for showing the currently selected difficulty level.
# SPDX-FileCopyrightText: 2025 Melissa LeBlanc-Williams for Adafruit Industries
# SPDX-License-Identifier: MIT
from displayio import Group
from adafruit_display_shapes.rect import Rect
from eventbutton import EventButton
MENU_ITEM_HEIGHT = 16
class Menu(Group):
def handle_mouse(self, point, clicked, waiting_for_release):
if waiting_for_release:
return False
# Check if the point is in the menu items group
handled_submenu = None
for submenu in self:
if isinstance(submenu, SubMenu):
if submenu.handle_mouse(point, clicked):
handled_submenu = submenu
if clicked:
# Hide any visible menus
for submenu in self:
if isinstance(submenu, SubMenu) and submenu != handled_submenu:
submenu.hide()
return handled_submenu is not None
class SubMenu(Group):
def __init__(self, label, button_width, menu_width, x, y):
super().__init__()
self._label = label
self._button_width = button_width
self._menu_width = menu_width
self._menu_items_group = None
self._xpos = x
self._ypos = y
self._menu_items = []
self._root_button = None
def add_item(self, function, label, selected=False):
key = label.lower().replace(" ", "_")
self._menu_items.append(
{
"key": key,
"function": function,
"label": label,
"selected": selected,
}
)
self._render()
def select_item(self, key):
for item in self._menu_items:
if item["key"] == key:
item["selected"] = True
else:
item["selected"] = False
self._render()
@staticmethod
def _create_button(callback, label, width, x, y=0, border=True, selected=False):
if border:
outline_color = 0x000000
selected_outline = 0x333333
else:
outline_color = 0xEEEEEE
selected_outline = 0xBBBBBB
if selected:
selected_label = label_color = 0x008800
else:
selected_label = label_color = 0x333333
button = EventButton(
callback,
x=x,
y=y,
width=width,
height=MENU_ITEM_HEIGHT,
label=label,
style=EventButton.RECT,
fill_color=0xEEEEEE,
outline_color=outline_color,
label_color=label_color,
selected_fill=0xBBBBBB,
selected_label=selected_label,
selected_outline=selected_outline,
)
return button
def _toggle_submenu(self):
self._menu_items_group.hidden = not self._menu_items_group.hidden
def _render(self):
# Redraw the menu
# Remove all existing elements contained inside of this class
while len(self) > 0:
self.pop()
# create a new root button
self._root_button = self._create_button(
self._toggle_submenu,
self._label,
self._button_width,
self._xpos,
self._ypos,
True,
)
self.append(self._root_button)
# Create the menu items group
self._menu_items_group = Group()
self._menu_items_group.hidden = True
self.append(self._menu_items_group)
# Add the background rectangle to the menu items group
self._menu_items_group.append(
Rect(self._xpos, self._ypos + self._root_button.height - 1, self._menu_width,
len(self._menu_items) * MENU_ITEM_HEIGHT + 2,
fill=0xEEEEEE,
outline=0x333333
)
)
# Add the menu items to the menu items group
for index, item in enumerate(self._menu_items):
button = self._create_button(
item["function"],
item["label"],
self._menu_width - 2,
self._xpos + 1,
self._ypos + index * MENU_ITEM_HEIGHT + self._root_button.height,
False,
item["selected"],
)
self._menu_items_group.append(button)
def hide(self):
self._menu_items_group.hidden = True
def handle_mouse(self, point, clicked):
# Check if the point is in the root button
if self._menu_items_group.hidden:
if self._root_button.contains(point):
self._root_button.selected = True
if clicked:
self._root_button.click()
return True
else:
self._root_button.selected = False
else:
# Check if the point is in the menu items group
for button in self._menu_items_group:
if isinstance(button, EventButton):
if button.contains(point):
button.selected = True
if clicked:
button.click()
self._menu_items_group.hidden = True
return True
else:
button.selected = False
return False
@property
def visible(self):
return not self._menu_items_group.hidden
@property
def items_group(self):
return self._menu_items_group
Page last edited June 02, 2025
Text editor powered by tinymce.