bauiv1lib.mainmenu
Implements the main menu window.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Implements the main menu window.""" 4 5from __future__ import annotations 6 7from typing import TYPE_CHECKING, override 8import logging 9 10import bauiv1 as bui 11import bascenev1 as bs 12 13if TYPE_CHECKING: 14 from typing import Any, Callable 15 16 17class MainMenuWindow(bui.MainWindow): 18 """The main menu window.""" 19 20 def __init__( 21 self, 22 transition: str | None = 'in_right', 23 origin_widget: bui.Widget | None = None, 24 ): 25 26 # Preload some modules we use in a background thread so we won't 27 # have a visual hitch when the user taps them. 28 bui.app.threadpool.submit_no_wait(self._preload_modules) 29 30 bui.set_analytics_screen('Main Menu') 31 self._show_remote_app_info_on_first_launch() 32 33 uiscale = bui.app.ui_v1.uiscale 34 35 # Make a vanilla container; we'll modify it to our needs in 36 # refresh. 37 super().__init__( 38 root_widget=bui.containerwidget( 39 toolbar_visibility=('menu_full_no_back') 40 ), 41 transition=transition, 42 origin_widget=origin_widget, 43 # We're affected by screen size only at small ui-scale. 44 refresh_on_screen_size_changes=uiscale is bui.UIScale.SMALL, 45 ) 46 47 # Grab this stuff in case it changes. 48 self._is_demo = bui.app.env.demo 49 self._is_arcade = bui.app.env.arcade 50 51 self._tdelay = 0.0 52 self._t_delay_inc = 0.02 53 self._t_delay_play = 1.7 54 self._use_autoselect = True 55 self._button_width = 200.0 56 self._button_height = 45.0 57 self._width = 100.0 58 self._height = 100.0 59 self._demo_menu_button: bui.Widget | None = None 60 self._gather_button: bui.Widget | None = None 61 self._play_button: bui.Widget | None = None 62 self._watch_button: bui.Widget | None = None 63 self._how_to_play_button: bui.Widget | None = None 64 self._credits_button: bui.Widget | None = None 65 66 self._refresh() 67 68 self._restore_state() 69 70 @override 71 def on_main_window_close(self) -> None: 72 self._save_state() 73 74 @override 75 def get_main_window_state(self) -> bui.MainWindowState: 76 # Support recreating our window for back/refresh purposes. 77 cls = type(self) 78 return bui.BasicMainWindowState( 79 create_call=lambda transition, origin_widget: cls( 80 transition=transition, origin_widget=origin_widget 81 ) 82 ) 83 84 @staticmethod 85 def _preload_modules() -> None: 86 """Preload modules we use; avoids hitches (called in bg thread).""" 87 # pylint: disable=cyclic-import 88 import bauiv1lib.getremote as _unused 89 import bauiv1lib.confirm as _unused2 90 import bauiv1lib.account.settings as _unused5 91 import bauiv1lib.store.browser as _unused6 92 import bauiv1lib.credits as _unused7 93 import bauiv1lib.help as _unused8 94 import bauiv1lib.settings.allsettings as _unused9 95 import bauiv1lib.gather as _unused10 96 import bauiv1lib.watch as _unused11 97 import bauiv1lib.play as _unused12 98 99 def _show_remote_app_info_on_first_launch(self) -> None: 100 app = bui.app 101 assert app.classic is not None 102 103 # The first time the non-in-game menu pops up, we might wanna 104 # show a 'get-remote-app' dialog in front of it. 105 if app.classic.first_main_menu: 106 app.classic.first_main_menu = False 107 try: 108 force_test = False 109 bs.get_local_active_input_devices_count() 110 if ( 111 (app.env.tv or app.classic.platform == 'mac') 112 and bui.app.config.get('launchCount', 0) <= 1 113 ) or force_test: 114 115 def _check_show_bs_remote_window() -> None: 116 try: 117 from bauiv1lib.getremote import GetBSRemoteWindow 118 119 bui.getsound('swish').play() 120 GetBSRemoteWindow() 121 except Exception: 122 logging.exception( 123 'Error showing get-remote window.' 124 ) 125 126 bui.apptimer(2.5, _check_show_bs_remote_window) 127 except Exception: 128 logging.exception('Error showing get-remote-app info.') 129 130 def get_play_button(self) -> bui.Widget | None: 131 """Return the play button.""" 132 return self._play_button 133 134 def _refresh(self) -> None: 135 # pylint: disable=too-many-statements 136 # pylint: disable=too-many-locals 137 138 classic = bui.app.classic 139 assert classic is not None 140 141 # Clear everything that was there. 142 children = self._root_widget.get_children() 143 for child in children: 144 child.delete() 145 146 self._tdelay = 0.0 147 self._t_delay_inc = 0.0 148 self._t_delay_play = 0.0 149 self._button_width = 200.0 150 self._button_height = 45.0 151 152 self._r = 'mainMenu' 153 154 app = bui.app 155 assert app.classic is not None 156 uiscale = app.ui_v1.uiscale 157 158 # Temp note about UI changes. 159 if bool(False): 160 bui.textwidget( 161 parent=self._root_widget, 162 position=( 163 (-400, 400) 164 if uiscale is bui.UIScale.LARGE 165 else ( 166 (-270, 320) 167 if uiscale is bui.UIScale.MEDIUM 168 else (-280, 280) 169 ) 170 ), 171 size=(0, 0), 172 scale=0.4, 173 flatness=1.0, 174 text=( 175 'WARNING: This build contains a revamped UI\n' 176 'which is still a work-in-progress. A number\n' 177 'of features are not currently functional or\n' 178 'contain bugs. To go back to the stable legacy UI,\n' 179 'grab version 1.7.36 from ballistica.net' 180 ), 181 h_align='left', 182 v_align='top', 183 ) 184 185 self._have_quit_button = app.classic.platform in ( 186 'windows', 187 'mac', 188 'linux', 189 ) 190 191 if not classic.did_menu_intro: 192 self._tdelay = 1.6 193 self._t_delay_inc = 0.03 194 classic.did_menu_intro = True 195 196 td1 = 2 197 td2 = 1 198 td3 = 0 199 td4 = -1 200 td5 = -2 201 202 self._width = 400.0 203 self._height = 200.0 204 205 play_button_width = self._button_width * 0.65 206 play_button_height = self._button_height * 1.1 207 play_button_scale = 1.7 208 hspace = 20.0 209 side_button_width = self._button_width * 0.4 210 side_button_height = side_button_width 211 side_button_scale = 0.95 212 side_button_y_offs = 5.0 213 hspace2 = 15.0 214 side_button_2_width = self._button_width * 1.0 215 side_button_2_height = side_button_2_width * 0.3 216 side_button_2_y_offs = 10.0 217 side_button_2_scale = 0.5 218 219 if uiscale is bui.UIScale.SMALL: 220 # We're a generally widescreen shaped window, so bump our 221 # overall scale up a bit when screen width is wider than safe 222 # bounds to take advantage of the extra space. 223 screensize = bui.get_virtual_screen_size() 224 safesize = bui.get_virtual_safe_area_size() 225 root_widget_scale = min(1.55, 1.3 * screensize[0] / safesize[0]) 226 button_y_offs = -20.0 227 self._button_height *= 1.3 228 elif uiscale is bui.UIScale.MEDIUM: 229 root_widget_scale = 1.3 230 button_y_offs = -55.0 231 self._button_height *= 1.25 232 else: 233 root_widget_scale = 1.0 234 button_y_offs = -90.0 235 self._button_height *= 1.2 236 237 bui.containerwidget( 238 edit=self._root_widget, 239 size=(self._width, self._height), 240 background=False, 241 scale=root_widget_scale, 242 ) 243 244 # Version/copyright info. 245 thistdelay = self._tdelay + td3 * self._t_delay_inc 246 bui.textwidget( 247 parent=self._root_widget, 248 position=(self._width * 0.5, button_y_offs - 10), 249 size=(0, 0), 250 scale=0.4, 251 flatness=1.0, 252 color=(1, 1, 1, 0.3), 253 text=( 254 f'{app.env.engine_version}' 255 f' build {app.env.engine_build_number}.' 256 f' Copyright 2025 Eric Froemling.' 257 ), 258 h_align='center', 259 v_align='center', 260 # transition_delay=self._t_delay_play, 261 transition_delay=thistdelay, 262 ) 263 264 # In kiosk mode, provide a button to get back to the kiosk menu. 265 if bui.app.env.demo or bui.app.env.arcade: 266 # h, v, scale = positions[self._p_index] 267 h = self._width * 0.5 268 v = button_y_offs 269 scale = 1.0 270 this_b_width = self._button_width * 0.4 * scale 271 # demo_menu_delay = ( 272 # 0.0 273 # if self._t_delay_play == 0.0 274 # else max(0, self._t_delay_play + 0.1) 275 # ) 276 demo_menu_delay = 0.0 277 self._demo_menu_button = bui.buttonwidget( 278 parent=self._root_widget, 279 id='demo', 280 position=(self._width * 0.5 - this_b_width * 0.5, v + 90), 281 size=(this_b_width, 45), 282 autoselect=True, 283 color=(0.45, 0.55, 0.45), 284 textcolor=(0.7, 0.8, 0.7), 285 label=bui.Lstr( 286 resource=( 287 'modeArcadeText' 288 if bui.app.env.arcade 289 else 'modeDemoText' 290 ) 291 ), 292 transition_delay=demo_menu_delay, 293 on_activate_call=self.main_window_back, 294 ) 295 else: 296 self._demo_menu_button = None 297 298 # Gather button 299 h = self._width * 0.5 300 h = ( 301 self._width * 0.5 302 - play_button_width * play_button_scale * 0.5 303 - hspace 304 - side_button_width * side_button_scale * 0.5 305 ) 306 v = button_y_offs + side_button_y_offs 307 308 thistdelay = self._tdelay + td2 * self._t_delay_inc 309 self._gather_button = btn = bui.buttonwidget( 310 parent=self._root_widget, 311 position=(h - side_button_width * side_button_scale * 0.5, v), 312 size=(side_button_width, side_button_height), 313 scale=side_button_scale, 314 autoselect=self._use_autoselect, 315 button_type='square', 316 label='', 317 transition_delay=thistdelay, 318 on_activate_call=self._gather_press, 319 ) 320 bui.textwidget( 321 parent=self._root_widget, 322 position=(h, v + side_button_height * side_button_scale * 0.25), 323 size=(0, 0), 324 scale=0.75, 325 transition_delay=thistdelay, 326 draw_controller=btn, 327 color=(0.75, 1.0, 0.7), 328 maxwidth=side_button_width * side_button_scale * 0.8, 329 text=bui.Lstr(resource='gatherWindow.titleText'), 330 h_align='center', 331 v_align='center', 332 ) 333 icon_size = side_button_width * side_button_scale * 0.63 334 bui.imagewidget( 335 parent=self._root_widget, 336 size=(icon_size, icon_size), 337 draw_controller=btn, 338 transition_delay=thistdelay, 339 position=( 340 h - 0.5 * icon_size, 341 v 342 + 0.65 * side_button_height * side_button_scale 343 - 0.5 * icon_size, 344 ), 345 texture=bui.gettexture('usersButton'), 346 ) 347 thistdelay = self._tdelay + td1 * self._t_delay_inc 348 349 h -= ( 350 side_button_width * side_button_scale * 0.5 351 + hspace2 352 + side_button_2_width * side_button_2_scale 353 ) 354 v = button_y_offs + side_button_2_y_offs 355 356 btn = bui.buttonwidget( 357 parent=self._root_widget, 358 id='howtoplay', 359 position=(h, v), 360 autoselect=self._use_autoselect, 361 size=(side_button_2_width, side_button_2_height * 2.0), 362 button_type='square', 363 scale=side_button_2_scale, 364 label=bui.Lstr(resource=f'{self._r}.howToPlayText'), 365 transition_delay=thistdelay, 366 on_activate_call=self._howtoplay, 367 ) 368 self._how_to_play_button = btn 369 370 # Play button. 371 h = self._width * 0.5 372 v = button_y_offs 373 assert play_button_width is not None 374 assert play_button_height is not None 375 thistdelay = self._tdelay + td3 * self._t_delay_inc 376 self._play_button = start_button = bui.buttonwidget( 377 parent=self._root_widget, 378 position=(h - play_button_width * 0.5 * play_button_scale, v), 379 size=(play_button_width, play_button_height), 380 autoselect=self._use_autoselect, 381 scale=play_button_scale, 382 text_res_scale=2.0, 383 label=bui.Lstr(resource='playText'), 384 transition_delay=thistdelay, 385 on_activate_call=self._play_press, 386 ) 387 bui.containerwidget( 388 edit=self._root_widget, 389 start_button=start_button, 390 selected_child=start_button, 391 ) 392 393 # self._tdelay += self._t_delay_inc 394 395 h = ( 396 self._width * 0.5 397 + play_button_width * play_button_scale * 0.5 398 + hspace 399 + side_button_width * side_button_scale * 0.5 400 ) 401 v = button_y_offs + side_button_y_offs 402 thistdelay = self._tdelay + td4 * self._t_delay_inc 403 self._watch_button = btn = bui.buttonwidget( 404 parent=self._root_widget, 405 position=(h - side_button_width * side_button_scale * 0.5, v), 406 size=(side_button_width, side_button_height), 407 scale=side_button_scale, 408 autoselect=self._use_autoselect, 409 button_type='square', 410 label='', 411 transition_delay=thistdelay, 412 on_activate_call=self._watch_press, 413 ) 414 bui.textwidget( 415 parent=self._root_widget, 416 position=(h, v + side_button_height * side_button_scale * 0.25), 417 size=(0, 0), 418 scale=0.75, 419 transition_delay=thistdelay, 420 color=(0.75, 1.0, 0.7), 421 draw_controller=btn, 422 maxwidth=side_button_width * side_button_scale * 0.8, 423 text=bui.Lstr(resource='watchWindow.titleText'), 424 h_align='center', 425 v_align='center', 426 ) 427 icon_size = side_button_width * side_button_scale * 0.63 428 bui.imagewidget( 429 parent=self._root_widget, 430 size=(icon_size, icon_size), 431 draw_controller=btn, 432 transition_delay=thistdelay, 433 position=( 434 h - 0.5 * icon_size, 435 v 436 + 0.65 * side_button_height * side_button_scale 437 - 0.5 * icon_size, 438 ), 439 texture=bui.gettexture('tv'), 440 ) 441 442 # Credits button. 443 thistdelay = self._tdelay + td5 * self._t_delay_inc 444 445 h += side_button_width * side_button_scale * 0.5 + hspace2 446 v = button_y_offs + side_button_2_y_offs 447 448 if self._have_quit_button: 449 v += 1.17 * side_button_2_height * side_button_2_scale 450 451 self._credits_button = bui.buttonwidget( 452 parent=self._root_widget, 453 position=(h, v), 454 button_type=None if self._have_quit_button else 'square', 455 size=( 456 side_button_2_width, 457 side_button_2_height * (1.0 if self._have_quit_button else 2.0), 458 ), 459 scale=side_button_2_scale, 460 autoselect=self._use_autoselect, 461 label=bui.Lstr(resource=f'{self._r}.creditsText'), 462 transition_delay=thistdelay, 463 on_activate_call=self._credits, 464 ) 465 466 self._quit_button: bui.Widget | None 467 if self._have_quit_button: 468 v -= 1.1 * side_button_2_height * side_button_2_scale 469 # Nudge this a tiny bit right so we can press right from the 470 # credits button to get to it. 471 self._quit_button = quit_button = bui.buttonwidget( 472 parent=self._root_widget, 473 autoselect=self._use_autoselect, 474 position=(h + 4.0, v), 475 size=(side_button_2_width, side_button_2_height), 476 scale=side_button_2_scale, 477 label=bui.Lstr( 478 resource=self._r 479 + ( 480 '.quitText' 481 if 'Mac' in app.classic.legacy_user_agent_string 482 else '.exitGameText' 483 ) 484 ), 485 on_activate_call=self._quit, 486 transition_delay=thistdelay, 487 ) 488 489 bui.containerwidget( 490 edit=self._root_widget, cancel_button=quit_button 491 ) 492 # self._tdelay += self._t_delay_inc 493 else: 494 self._quit_button = None 495 496 # If we're not in-game, have no quit button, and this is 497 # android, we want back presses to quit our activity. 498 if app.classic.platform == 'android': 499 500 def _do_quit() -> None: 501 bui.quit(confirm=True, quit_type=bui.QuitType.BACK) 502 503 bui.containerwidget( 504 edit=self._root_widget, on_cancel_call=_do_quit 505 ) 506 507 def _quit(self) -> None: 508 # pylint: disable=cyclic-import 509 from bauiv1lib.confirm import QuitWindow 510 511 # no-op if we're not currently in control. 512 if not self.main_window_has_control(): 513 return 514 515 # Note: Normally we should go through bui.quit(confirm=True) but 516 # invoking the window directly lets us scale it up from the 517 # button. 518 QuitWindow(origin_widget=self._quit_button) 519 520 def _credits(self) -> None: 521 # pylint: disable=cyclic-import 522 from bauiv1lib.credits import CreditsWindow 523 524 # no-op if we're not currently in control. 525 if not self.main_window_has_control(): 526 return 527 528 self.main_window_replace( 529 CreditsWindow(origin_widget=self._credits_button), 530 ) 531 532 def _howtoplay(self) -> None: 533 # pylint: disable=cyclic-import 534 from bauiv1lib.help import HelpWindow 535 536 # no-op if we're not currently in control. 537 if not self.main_window_has_control(): 538 return 539 540 self.main_window_replace( 541 HelpWindow(origin_widget=self._how_to_play_button), 542 ) 543 544 def _save_state(self) -> None: 545 try: 546 sel = self._root_widget.get_selected_child() 547 if sel == self._play_button: 548 sel_name = 'Start' 549 elif sel == self._gather_button: 550 sel_name = 'Gather' 551 elif sel == self._watch_button: 552 sel_name = 'Watch' 553 elif sel == self._how_to_play_button: 554 sel_name = 'HowToPlay' 555 elif sel == self._credits_button: 556 sel_name = 'Credits' 557 elif sel == self._quit_button: 558 sel_name = 'Quit' 559 elif sel == self._demo_menu_button: 560 sel_name = 'DemoMenu' 561 else: 562 print(f'Unknown widget in main menu selection: {sel}.') 563 sel_name = 'Start' 564 bui.app.ui_v1.window_states[type(self)] = {'sel_name': sel_name} 565 except Exception: 566 logging.exception('Error saving state for %s.', self) 567 568 def _restore_state(self) -> None: 569 try: 570 571 sel: bui.Widget | None 572 573 sel_name = bui.app.ui_v1.window_states.get(type(self), {}).get( 574 'sel_name' 575 ) 576 assert isinstance(sel_name, (str, type(None))) 577 if sel_name is None: 578 sel_name = 'Start' 579 if sel_name == 'HowToPlay': 580 sel = self._how_to_play_button 581 elif sel_name == 'Gather': 582 sel = self._gather_button 583 elif sel_name == 'Watch': 584 sel = self._watch_button 585 elif sel_name == 'Credits': 586 sel = self._credits_button 587 elif sel_name == 'Quit': 588 sel = self._quit_button 589 elif sel_name == 'DemoMenu': 590 sel = self._demo_menu_button 591 else: 592 sel = self._play_button 593 if sel is not None: 594 bui.containerwidget(edit=self._root_widget, selected_child=sel) 595 596 except Exception: 597 logging.exception('Error restoring state for %s.', self) 598 599 def _gather_press(self) -> None: 600 # pylint: disable=cyclic-import 601 from bauiv1lib.gather import GatherWindow 602 603 # no-op if we're not currently in control. 604 if not self.main_window_has_control(): 605 return 606 607 self.main_window_replace( 608 GatherWindow(origin_widget=self._gather_button) 609 ) 610 611 def _watch_press(self) -> None: 612 # pylint: disable=cyclic-import 613 from bauiv1lib.watch import WatchWindow 614 615 # no-op if we're not currently in control. 616 if not self.main_window_has_control(): 617 return 618 619 self.main_window_replace( 620 WatchWindow(origin_widget=self._watch_button), 621 ) 622 623 def _play_press(self) -> None: 624 # pylint: disable=cyclic-import 625 from bauiv1lib.play import PlayWindow 626 627 # no-op if we're not currently in control. 628 if not self.main_window_has_control(): 629 return 630 631 self.main_window_replace(PlayWindow(origin_widget=self._play_button))
class
MainMenuWindow(bauiv1._uitypes.MainWindow):
18class MainMenuWindow(bui.MainWindow): 19 """The main menu window.""" 20 21 def __init__( 22 self, 23 transition: str | None = 'in_right', 24 origin_widget: bui.Widget | None = None, 25 ): 26 27 # Preload some modules we use in a background thread so we won't 28 # have a visual hitch when the user taps them. 29 bui.app.threadpool.submit_no_wait(self._preload_modules) 30 31 bui.set_analytics_screen('Main Menu') 32 self._show_remote_app_info_on_first_launch() 33 34 uiscale = bui.app.ui_v1.uiscale 35 36 # Make a vanilla container; we'll modify it to our needs in 37 # refresh. 38 super().__init__( 39 root_widget=bui.containerwidget( 40 toolbar_visibility=('menu_full_no_back') 41 ), 42 transition=transition, 43 origin_widget=origin_widget, 44 # We're affected by screen size only at small ui-scale. 45 refresh_on_screen_size_changes=uiscale is bui.UIScale.SMALL, 46 ) 47 48 # Grab this stuff in case it changes. 49 self._is_demo = bui.app.env.demo 50 self._is_arcade = bui.app.env.arcade 51 52 self._tdelay = 0.0 53 self._t_delay_inc = 0.02 54 self._t_delay_play = 1.7 55 self._use_autoselect = True 56 self._button_width = 200.0 57 self._button_height = 45.0 58 self._width = 100.0 59 self._height = 100.0 60 self._demo_menu_button: bui.Widget | None = None 61 self._gather_button: bui.Widget | None = None 62 self._play_button: bui.Widget | None = None 63 self._watch_button: bui.Widget | None = None 64 self._how_to_play_button: bui.Widget | None = None 65 self._credits_button: bui.Widget | None = None 66 67 self._refresh() 68 69 self._restore_state() 70 71 @override 72 def on_main_window_close(self) -> None: 73 self._save_state() 74 75 @override 76 def get_main_window_state(self) -> bui.MainWindowState: 77 # Support recreating our window for back/refresh purposes. 78 cls = type(self) 79 return bui.BasicMainWindowState( 80 create_call=lambda transition, origin_widget: cls( 81 transition=transition, origin_widget=origin_widget 82 ) 83 ) 84 85 @staticmethod 86 def _preload_modules() -> None: 87 """Preload modules we use; avoids hitches (called in bg thread).""" 88 # pylint: disable=cyclic-import 89 import bauiv1lib.getremote as _unused 90 import bauiv1lib.confirm as _unused2 91 import bauiv1lib.account.settings as _unused5 92 import bauiv1lib.store.browser as _unused6 93 import bauiv1lib.credits as _unused7 94 import bauiv1lib.help as _unused8 95 import bauiv1lib.settings.allsettings as _unused9 96 import bauiv1lib.gather as _unused10 97 import bauiv1lib.watch as _unused11 98 import bauiv1lib.play as _unused12 99 100 def _show_remote_app_info_on_first_launch(self) -> None: 101 app = bui.app 102 assert app.classic is not None 103 104 # The first time the non-in-game menu pops up, we might wanna 105 # show a 'get-remote-app' dialog in front of it. 106 if app.classic.first_main_menu: 107 app.classic.first_main_menu = False 108 try: 109 force_test = False 110 bs.get_local_active_input_devices_count() 111 if ( 112 (app.env.tv or app.classic.platform == 'mac') 113 and bui.app.config.get('launchCount', 0) <= 1 114 ) or force_test: 115 116 def _check_show_bs_remote_window() -> None: 117 try: 118 from bauiv1lib.getremote import GetBSRemoteWindow 119 120 bui.getsound('swish').play() 121 GetBSRemoteWindow() 122 except Exception: 123 logging.exception( 124 'Error showing get-remote window.' 125 ) 126 127 bui.apptimer(2.5, _check_show_bs_remote_window) 128 except Exception: 129 logging.exception('Error showing get-remote-app info.') 130 131 def get_play_button(self) -> bui.Widget | None: 132 """Return the play button.""" 133 return self._play_button 134 135 def _refresh(self) -> None: 136 # pylint: disable=too-many-statements 137 # pylint: disable=too-many-locals 138 139 classic = bui.app.classic 140 assert classic is not None 141 142 # Clear everything that was there. 143 children = self._root_widget.get_children() 144 for child in children: 145 child.delete() 146 147 self._tdelay = 0.0 148 self._t_delay_inc = 0.0 149 self._t_delay_play = 0.0 150 self._button_width = 200.0 151 self._button_height = 45.0 152 153 self._r = 'mainMenu' 154 155 app = bui.app 156 assert app.classic is not None 157 uiscale = app.ui_v1.uiscale 158 159 # Temp note about UI changes. 160 if bool(False): 161 bui.textwidget( 162 parent=self._root_widget, 163 position=( 164 (-400, 400) 165 if uiscale is bui.UIScale.LARGE 166 else ( 167 (-270, 320) 168 if uiscale is bui.UIScale.MEDIUM 169 else (-280, 280) 170 ) 171 ), 172 size=(0, 0), 173 scale=0.4, 174 flatness=1.0, 175 text=( 176 'WARNING: This build contains a revamped UI\n' 177 'which is still a work-in-progress. A number\n' 178 'of features are not currently functional or\n' 179 'contain bugs. To go back to the stable legacy UI,\n' 180 'grab version 1.7.36 from ballistica.net' 181 ), 182 h_align='left', 183 v_align='top', 184 ) 185 186 self._have_quit_button = app.classic.platform in ( 187 'windows', 188 'mac', 189 'linux', 190 ) 191 192 if not classic.did_menu_intro: 193 self._tdelay = 1.6 194 self._t_delay_inc = 0.03 195 classic.did_menu_intro = True 196 197 td1 = 2 198 td2 = 1 199 td3 = 0 200 td4 = -1 201 td5 = -2 202 203 self._width = 400.0 204 self._height = 200.0 205 206 play_button_width = self._button_width * 0.65 207 play_button_height = self._button_height * 1.1 208 play_button_scale = 1.7 209 hspace = 20.0 210 side_button_width = self._button_width * 0.4 211 side_button_height = side_button_width 212 side_button_scale = 0.95 213 side_button_y_offs = 5.0 214 hspace2 = 15.0 215 side_button_2_width = self._button_width * 1.0 216 side_button_2_height = side_button_2_width * 0.3 217 side_button_2_y_offs = 10.0 218 side_button_2_scale = 0.5 219 220 if uiscale is bui.UIScale.SMALL: 221 # We're a generally widescreen shaped window, so bump our 222 # overall scale up a bit when screen width is wider than safe 223 # bounds to take advantage of the extra space. 224 screensize = bui.get_virtual_screen_size() 225 safesize = bui.get_virtual_safe_area_size() 226 root_widget_scale = min(1.55, 1.3 * screensize[0] / safesize[0]) 227 button_y_offs = -20.0 228 self._button_height *= 1.3 229 elif uiscale is bui.UIScale.MEDIUM: 230 root_widget_scale = 1.3 231 button_y_offs = -55.0 232 self._button_height *= 1.25 233 else: 234 root_widget_scale = 1.0 235 button_y_offs = -90.0 236 self._button_height *= 1.2 237 238 bui.containerwidget( 239 edit=self._root_widget, 240 size=(self._width, self._height), 241 background=False, 242 scale=root_widget_scale, 243 ) 244 245 # Version/copyright info. 246 thistdelay = self._tdelay + td3 * self._t_delay_inc 247 bui.textwidget( 248 parent=self._root_widget, 249 position=(self._width * 0.5, button_y_offs - 10), 250 size=(0, 0), 251 scale=0.4, 252 flatness=1.0, 253 color=(1, 1, 1, 0.3), 254 text=( 255 f'{app.env.engine_version}' 256 f' build {app.env.engine_build_number}.' 257 f' Copyright 2025 Eric Froemling.' 258 ), 259 h_align='center', 260 v_align='center', 261 # transition_delay=self._t_delay_play, 262 transition_delay=thistdelay, 263 ) 264 265 # In kiosk mode, provide a button to get back to the kiosk menu. 266 if bui.app.env.demo or bui.app.env.arcade: 267 # h, v, scale = positions[self._p_index] 268 h = self._width * 0.5 269 v = button_y_offs 270 scale = 1.0 271 this_b_width = self._button_width * 0.4 * scale 272 # demo_menu_delay = ( 273 # 0.0 274 # if self._t_delay_play == 0.0 275 # else max(0, self._t_delay_play + 0.1) 276 # ) 277 demo_menu_delay = 0.0 278 self._demo_menu_button = bui.buttonwidget( 279 parent=self._root_widget, 280 id='demo', 281 position=(self._width * 0.5 - this_b_width * 0.5, v + 90), 282 size=(this_b_width, 45), 283 autoselect=True, 284 color=(0.45, 0.55, 0.45), 285 textcolor=(0.7, 0.8, 0.7), 286 label=bui.Lstr( 287 resource=( 288 'modeArcadeText' 289 if bui.app.env.arcade 290 else 'modeDemoText' 291 ) 292 ), 293 transition_delay=demo_menu_delay, 294 on_activate_call=self.main_window_back, 295 ) 296 else: 297 self._demo_menu_button = None 298 299 # Gather button 300 h = self._width * 0.5 301 h = ( 302 self._width * 0.5 303 - play_button_width * play_button_scale * 0.5 304 - hspace 305 - side_button_width * side_button_scale * 0.5 306 ) 307 v = button_y_offs + side_button_y_offs 308 309 thistdelay = self._tdelay + td2 * self._t_delay_inc 310 self._gather_button = btn = bui.buttonwidget( 311 parent=self._root_widget, 312 position=(h - side_button_width * side_button_scale * 0.5, v), 313 size=(side_button_width, side_button_height), 314 scale=side_button_scale, 315 autoselect=self._use_autoselect, 316 button_type='square', 317 label='', 318 transition_delay=thistdelay, 319 on_activate_call=self._gather_press, 320 ) 321 bui.textwidget( 322 parent=self._root_widget, 323 position=(h, v + side_button_height * side_button_scale * 0.25), 324 size=(0, 0), 325 scale=0.75, 326 transition_delay=thistdelay, 327 draw_controller=btn, 328 color=(0.75, 1.0, 0.7), 329 maxwidth=side_button_width * side_button_scale * 0.8, 330 text=bui.Lstr(resource='gatherWindow.titleText'), 331 h_align='center', 332 v_align='center', 333 ) 334 icon_size = side_button_width * side_button_scale * 0.63 335 bui.imagewidget( 336 parent=self._root_widget, 337 size=(icon_size, icon_size), 338 draw_controller=btn, 339 transition_delay=thistdelay, 340 position=( 341 h - 0.5 * icon_size, 342 v 343 + 0.65 * side_button_height * side_button_scale 344 - 0.5 * icon_size, 345 ), 346 texture=bui.gettexture('usersButton'), 347 ) 348 thistdelay = self._tdelay + td1 * self._t_delay_inc 349 350 h -= ( 351 side_button_width * side_button_scale * 0.5 352 + hspace2 353 + side_button_2_width * side_button_2_scale 354 ) 355 v = button_y_offs + side_button_2_y_offs 356 357 btn = bui.buttonwidget( 358 parent=self._root_widget, 359 id='howtoplay', 360 position=(h, v), 361 autoselect=self._use_autoselect, 362 size=(side_button_2_width, side_button_2_height * 2.0), 363 button_type='square', 364 scale=side_button_2_scale, 365 label=bui.Lstr(resource=f'{self._r}.howToPlayText'), 366 transition_delay=thistdelay, 367 on_activate_call=self._howtoplay, 368 ) 369 self._how_to_play_button = btn 370 371 # Play button. 372 h = self._width * 0.5 373 v = button_y_offs 374 assert play_button_width is not None 375 assert play_button_height is not None 376 thistdelay = self._tdelay + td3 * self._t_delay_inc 377 self._play_button = start_button = bui.buttonwidget( 378 parent=self._root_widget, 379 position=(h - play_button_width * 0.5 * play_button_scale, v), 380 size=(play_button_width, play_button_height), 381 autoselect=self._use_autoselect, 382 scale=play_button_scale, 383 text_res_scale=2.0, 384 label=bui.Lstr(resource='playText'), 385 transition_delay=thistdelay, 386 on_activate_call=self._play_press, 387 ) 388 bui.containerwidget( 389 edit=self._root_widget, 390 start_button=start_button, 391 selected_child=start_button, 392 ) 393 394 # self._tdelay += self._t_delay_inc 395 396 h = ( 397 self._width * 0.5 398 + play_button_width * play_button_scale * 0.5 399 + hspace 400 + side_button_width * side_button_scale * 0.5 401 ) 402 v = button_y_offs + side_button_y_offs 403 thistdelay = self._tdelay + td4 * self._t_delay_inc 404 self._watch_button = btn = bui.buttonwidget( 405 parent=self._root_widget, 406 position=(h - side_button_width * side_button_scale * 0.5, v), 407 size=(side_button_width, side_button_height), 408 scale=side_button_scale, 409 autoselect=self._use_autoselect, 410 button_type='square', 411 label='', 412 transition_delay=thistdelay, 413 on_activate_call=self._watch_press, 414 ) 415 bui.textwidget( 416 parent=self._root_widget, 417 position=(h, v + side_button_height * side_button_scale * 0.25), 418 size=(0, 0), 419 scale=0.75, 420 transition_delay=thistdelay, 421 color=(0.75, 1.0, 0.7), 422 draw_controller=btn, 423 maxwidth=side_button_width * side_button_scale * 0.8, 424 text=bui.Lstr(resource='watchWindow.titleText'), 425 h_align='center', 426 v_align='center', 427 ) 428 icon_size = side_button_width * side_button_scale * 0.63 429 bui.imagewidget( 430 parent=self._root_widget, 431 size=(icon_size, icon_size), 432 draw_controller=btn, 433 transition_delay=thistdelay, 434 position=( 435 h - 0.5 * icon_size, 436 v 437 + 0.65 * side_button_height * side_button_scale 438 - 0.5 * icon_size, 439 ), 440 texture=bui.gettexture('tv'), 441 ) 442 443 # Credits button. 444 thistdelay = self._tdelay + td5 * self._t_delay_inc 445 446 h += side_button_width * side_button_scale * 0.5 + hspace2 447 v = button_y_offs + side_button_2_y_offs 448 449 if self._have_quit_button: 450 v += 1.17 * side_button_2_height * side_button_2_scale 451 452 self._credits_button = bui.buttonwidget( 453 parent=self._root_widget, 454 position=(h, v), 455 button_type=None if self._have_quit_button else 'square', 456 size=( 457 side_button_2_width, 458 side_button_2_height * (1.0 if self._have_quit_button else 2.0), 459 ), 460 scale=side_button_2_scale, 461 autoselect=self._use_autoselect, 462 label=bui.Lstr(resource=f'{self._r}.creditsText'), 463 transition_delay=thistdelay, 464 on_activate_call=self._credits, 465 ) 466 467 self._quit_button: bui.Widget | None 468 if self._have_quit_button: 469 v -= 1.1 * side_button_2_height * side_button_2_scale 470 # Nudge this a tiny bit right so we can press right from the 471 # credits button to get to it. 472 self._quit_button = quit_button = bui.buttonwidget( 473 parent=self._root_widget, 474 autoselect=self._use_autoselect, 475 position=(h + 4.0, v), 476 size=(side_button_2_width, side_button_2_height), 477 scale=side_button_2_scale, 478 label=bui.Lstr( 479 resource=self._r 480 + ( 481 '.quitText' 482 if 'Mac' in app.classic.legacy_user_agent_string 483 else '.exitGameText' 484 ) 485 ), 486 on_activate_call=self._quit, 487 transition_delay=thistdelay, 488 ) 489 490 bui.containerwidget( 491 edit=self._root_widget, cancel_button=quit_button 492 ) 493 # self._tdelay += self._t_delay_inc 494 else: 495 self._quit_button = None 496 497 # If we're not in-game, have no quit button, and this is 498 # android, we want back presses to quit our activity. 499 if app.classic.platform == 'android': 500 501 def _do_quit() -> None: 502 bui.quit(confirm=True, quit_type=bui.QuitType.BACK) 503 504 bui.containerwidget( 505 edit=self._root_widget, on_cancel_call=_do_quit 506 ) 507 508 def _quit(self) -> None: 509 # pylint: disable=cyclic-import 510 from bauiv1lib.confirm import QuitWindow 511 512 # no-op if we're not currently in control. 513 if not self.main_window_has_control(): 514 return 515 516 # Note: Normally we should go through bui.quit(confirm=True) but 517 # invoking the window directly lets us scale it up from the 518 # button. 519 QuitWindow(origin_widget=self._quit_button) 520 521 def _credits(self) -> None: 522 # pylint: disable=cyclic-import 523 from bauiv1lib.credits import CreditsWindow 524 525 # no-op if we're not currently in control. 526 if not self.main_window_has_control(): 527 return 528 529 self.main_window_replace( 530 CreditsWindow(origin_widget=self._credits_button), 531 ) 532 533 def _howtoplay(self) -> None: 534 # pylint: disable=cyclic-import 535 from bauiv1lib.help import HelpWindow 536 537 # no-op if we're not currently in control. 538 if not self.main_window_has_control(): 539 return 540 541 self.main_window_replace( 542 HelpWindow(origin_widget=self._how_to_play_button), 543 ) 544 545 def _save_state(self) -> None: 546 try: 547 sel = self._root_widget.get_selected_child() 548 if sel == self._play_button: 549 sel_name = 'Start' 550 elif sel == self._gather_button: 551 sel_name = 'Gather' 552 elif sel == self._watch_button: 553 sel_name = 'Watch' 554 elif sel == self._how_to_play_button: 555 sel_name = 'HowToPlay' 556 elif sel == self._credits_button: 557 sel_name = 'Credits' 558 elif sel == self._quit_button: 559 sel_name = 'Quit' 560 elif sel == self._demo_menu_button: 561 sel_name = 'DemoMenu' 562 else: 563 print(f'Unknown widget in main menu selection: {sel}.') 564 sel_name = 'Start' 565 bui.app.ui_v1.window_states[type(self)] = {'sel_name': sel_name} 566 except Exception: 567 logging.exception('Error saving state for %s.', self) 568 569 def _restore_state(self) -> None: 570 try: 571 572 sel: bui.Widget | None 573 574 sel_name = bui.app.ui_v1.window_states.get(type(self), {}).get( 575 'sel_name' 576 ) 577 assert isinstance(sel_name, (str, type(None))) 578 if sel_name is None: 579 sel_name = 'Start' 580 if sel_name == 'HowToPlay': 581 sel = self._how_to_play_button 582 elif sel_name == 'Gather': 583 sel = self._gather_button 584 elif sel_name == 'Watch': 585 sel = self._watch_button 586 elif sel_name == 'Credits': 587 sel = self._credits_button 588 elif sel_name == 'Quit': 589 sel = self._quit_button 590 elif sel_name == 'DemoMenu': 591 sel = self._demo_menu_button 592 else: 593 sel = self._play_button 594 if sel is not None: 595 bui.containerwidget(edit=self._root_widget, selected_child=sel) 596 597 except Exception: 598 logging.exception('Error restoring state for %s.', self) 599 600 def _gather_press(self) -> None: 601 # pylint: disable=cyclic-import 602 from bauiv1lib.gather import GatherWindow 603 604 # no-op if we're not currently in control. 605 if not self.main_window_has_control(): 606 return 607 608 self.main_window_replace( 609 GatherWindow(origin_widget=self._gather_button) 610 ) 611 612 def _watch_press(self) -> None: 613 # pylint: disable=cyclic-import 614 from bauiv1lib.watch import WatchWindow 615 616 # no-op if we're not currently in control. 617 if not self.main_window_has_control(): 618 return 619 620 self.main_window_replace( 621 WatchWindow(origin_widget=self._watch_button), 622 ) 623 624 def _play_press(self) -> None: 625 # pylint: disable=cyclic-import 626 from bauiv1lib.play import PlayWindow 627 628 # no-op if we're not currently in control. 629 if not self.main_window_has_control(): 630 return 631 632 self.main_window_replace(PlayWindow(origin_widget=self._play_button))
The main menu window.
MainMenuWindow( transition: str | None = 'in_right', origin_widget: _bauiv1.Widget | None = None)
21 def __init__( 22 self, 23 transition: str | None = 'in_right', 24 origin_widget: bui.Widget | None = None, 25 ): 26 27 # Preload some modules we use in a background thread so we won't 28 # have a visual hitch when the user taps them. 29 bui.app.threadpool.submit_no_wait(self._preload_modules) 30 31 bui.set_analytics_screen('Main Menu') 32 self._show_remote_app_info_on_first_launch() 33 34 uiscale = bui.app.ui_v1.uiscale 35 36 # Make a vanilla container; we'll modify it to our needs in 37 # refresh. 38 super().__init__( 39 root_widget=bui.containerwidget( 40 toolbar_visibility=('menu_full_no_back') 41 ), 42 transition=transition, 43 origin_widget=origin_widget, 44 # We're affected by screen size only at small ui-scale. 45 refresh_on_screen_size_changes=uiscale is bui.UIScale.SMALL, 46 ) 47 48 # Grab this stuff in case it changes. 49 self._is_demo = bui.app.env.demo 50 self._is_arcade = bui.app.env.arcade 51 52 self._tdelay = 0.0 53 self._t_delay_inc = 0.02 54 self._t_delay_play = 1.7 55 self._use_autoselect = True 56 self._button_width = 200.0 57 self._button_height = 45.0 58 self._width = 100.0 59 self._height = 100.0 60 self._demo_menu_button: bui.Widget | None = None 61 self._gather_button: bui.Widget | None = None 62 self._play_button: bui.Widget | None = None 63 self._watch_button: bui.Widget | None = None 64 self._how_to_play_button: bui.Widget | None = None 65 self._credits_button: bui.Widget | None = None 66 67 self._refresh() 68 69 self._restore_state()
Create a MainWindow given a root widget and transition info.
Automatically handles in and out transitions on the provided widget, so there is no need to set transitions when creating it.
@override
def
on_main_window_close(self) -> None:
Called before transitioning out a main window.
A good opportunity to save window state/etc.
75 @override 76 def get_main_window_state(self) -> bui.MainWindowState: 77 # Support recreating our window for back/refresh purposes. 78 cls = type(self) 79 return bui.BasicMainWindowState( 80 create_call=lambda transition, origin_widget: cls( 81 transition=transition, origin_widget=origin_widget 82 ) 83 )
Return a WindowState to recreate this window, if supported.