bauiv1lib.settings.graphics

Provides UI for graphics settings.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Provides UI for graphics settings."""
  4
  5from __future__ import annotations
  6
  7from typing import TYPE_CHECKING, cast, override
  8
  9from bauiv1lib.popup import PopupMenu
 10from bauiv1lib.config import ConfigCheckBox
 11import bauiv1 as bui
 12
 13if TYPE_CHECKING:
 14    from typing import Any
 15
 16
 17class GraphicsSettingsWindow(bui.MainWindow):
 18    """Window for graphics settings."""
 19
 20    def __init__(
 21        self,
 22        transition: str | None = 'in_right',
 23        origin_widget: bui.Widget | None = None,
 24    ):
 25        # pylint: disable=too-many-locals
 26        # pylint: disable=too-many-branches
 27        # pylint: disable=too-many-statements
 28
 29        self._r = 'graphicsSettingsWindow'
 30        app = bui.app
 31        assert app.classic is not None
 32
 33        spacing = 32
 34        self._have_selected_child = False
 35        uiscale = app.ui_v1.uiscale
 36        width = 450.0
 37        height = 302.0
 38        self._max_fps_dirty = False
 39        self._last_max_fps_set_time = bui.apptime()
 40        self._last_max_fps_str = ''
 41
 42        self._show_fullscreen = False
 43        fullscreen_spacing_top = spacing * 0.2
 44        fullscreen_spacing = spacing * 1.2
 45        if bui.fullscreen_control_available():
 46            self._show_fullscreen = True
 47            height += fullscreen_spacing + fullscreen_spacing_top
 48
 49        show_vsync = bui.supports_vsync()
 50        show_tv_mode = not bui.app.env.vr
 51
 52        show_max_fps = bui.supports_max_fps()
 53        if show_max_fps:
 54            height += 50
 55
 56        show_resolution = True
 57        if app.env.vr:
 58            show_resolution = (
 59                app.classic.platform == 'android'
 60                and app.classic.subplatform == 'cardboard'
 61            )
 62
 63        assert bui.app.classic is not None
 64        uiscale = bui.app.ui_v1.uiscale
 65        base_scale = (
 66            1.5
 67            if uiscale is bui.UIScale.SMALL
 68            else 1.3 if uiscale is bui.UIScale.MEDIUM else 1.0
 69        )
 70        popup_menu_scale = base_scale * 1.2
 71        v = height - 50
 72        v -= spacing * 1.15
 73        super().__init__(
 74            root_widget=bui.containerwidget(
 75                size=(width, height),
 76                scale=base_scale,
 77                stack_offset=(
 78                    (0, -10) if uiscale is bui.UIScale.SMALL else (0, 0)
 79                ),
 80                toolbar_visibility=(
 81                    None if uiscale is bui.UIScale.SMALL else 'menu_full'
 82                ),
 83            ),
 84            transition=transition,
 85            origin_widget=origin_widget,
 86        )
 87
 88        back_button = bui.buttonwidget(
 89            parent=self._root_widget,
 90            position=(35, height - 50),
 91            size=(60, 60),
 92            scale=0.8,
 93            text_scale=1.2,
 94            autoselect=True,
 95            label=bui.charstr(bui.SpecialChar.BACK),
 96            button_type='backSmall',
 97            on_activate_call=self.main_window_back,
 98        )
 99
100        bui.containerwidget(edit=self._root_widget, cancel_button=back_button)
101
102        bui.textwidget(
103            parent=self._root_widget,
104            position=(0, height - 44),
105            size=(width, 25),
106            text=bui.Lstr(resource=f'{self._r}.titleText'),
107            color=bui.app.ui_v1.title_color,
108            h_align='center',
109            v_align='top',
110        )
111
112        self._fullscreen_checkbox: bui.Widget | None = None
113        if self._show_fullscreen:
114            v -= fullscreen_spacing_top
115            # Fullscreen control does not necessarily talk to the
116            # app config so we have to wrangle it manually instead of
117            # using a config-checkbox.
118            label = bui.Lstr(resource=f'{self._r}.fullScreenText')
119
120            # Show keyboard shortcut alongside the control if they
121            # provide one.
122            shortcut = bui.fullscreen_control_key_shortcut()
123            if shortcut is not None:
124                label = bui.Lstr(
125                    value='$(NAME) [$(SHORTCUT)]',
126                    subs=[('$(NAME)', label), ('$(SHORTCUT)', shortcut)],
127                )
128            self._fullscreen_checkbox = bui.checkboxwidget(
129                parent=self._root_widget,
130                position=(100, v),
131                value=bui.fullscreen_control_get(),
132                on_value_change_call=bui.fullscreen_control_set,
133                maxwidth=250,
134                size=(300, 30),
135                text=label,
136            )
137
138            if not self._have_selected_child:
139                bui.containerwidget(
140                    edit=self._root_widget,
141                    selected_child=self._fullscreen_checkbox,
142                )
143                self._have_selected_child = True
144            v -= fullscreen_spacing
145
146        self._selected_color = (0.5, 1, 0.5, 1)
147        self._unselected_color = (0.7, 0.7, 0.7, 1)
148
149        # Quality
150        bui.textwidget(
151            parent=self._root_widget,
152            position=(60, v),
153            size=(160, 25),
154            text=bui.Lstr(resource=f'{self._r}.visualsText'),
155            color=bui.app.ui_v1.heading_color,
156            scale=0.65,
157            maxwidth=150,
158            h_align='center',
159            v_align='center',
160        )
161        PopupMenu(
162            parent=self._root_widget,
163            position=(60, v - 50),
164            width=150,
165            scale=popup_menu_scale,
166            choices=['Auto', 'Higher', 'High', 'Medium', 'Low'],
167            choices_disabled=(
168                ['Higher', 'High']
169                if bui.get_max_graphics_quality() == 'Medium'
170                else []
171            ),
172            choices_display=[
173                bui.Lstr(resource='autoText'),
174                bui.Lstr(resource=f'{self._r}.higherText'),
175                bui.Lstr(resource=f'{self._r}.highText'),
176                bui.Lstr(resource=f'{self._r}.mediumText'),
177                bui.Lstr(resource=f'{self._r}.lowText'),
178            ],
179            current_choice=bui.app.config.resolve('Graphics Quality'),
180            on_value_change_call=self._set_quality,
181        )
182
183        # Texture controls
184        bui.textwidget(
185            parent=self._root_widget,
186            position=(230, v),
187            size=(160, 25),
188            text=bui.Lstr(resource=f'{self._r}.texturesText'),
189            color=bui.app.ui_v1.heading_color,
190            scale=0.65,
191            maxwidth=150,
192            h_align='center',
193            v_align='center',
194        )
195        textures_popup = PopupMenu(
196            parent=self._root_widget,
197            position=(230, v - 50),
198            width=150,
199            scale=popup_menu_scale,
200            choices=['Auto', 'High', 'Medium', 'Low'],
201            choices_display=[
202                bui.Lstr(resource='autoText'),
203                bui.Lstr(resource=f'{self._r}.highText'),
204                bui.Lstr(resource=f'{self._r}.mediumText'),
205                bui.Lstr(resource=f'{self._r}.lowText'),
206            ],
207            current_choice=bui.app.config.resolve('Texture Quality'),
208            on_value_change_call=self._set_textures,
209        )
210        bui.widget(
211            edit=textures_popup.get_button(),
212            right_widget=bui.get_special_widget('squad_button'),
213        )
214        v -= 80
215
216        h_offs = 0
217
218        resolution_popup: PopupMenu | None = None
219
220        if show_resolution:
221            bui.textwidget(
222                parent=self._root_widget,
223                position=(h_offs + 60, v),
224                size=(160, 25),
225                text=bui.Lstr(resource=f'{self._r}.resolutionText'),
226                color=bui.app.ui_v1.heading_color,
227                scale=0.65,
228                maxwidth=150,
229                h_align='center',
230                v_align='center',
231            )
232
233            # On standard android we have 'Auto', 'Native', and a few
234            # HD standards.
235            if app.classic.platform == 'android':
236                # on cardboard/daydream android we have a few
237                # render-target-scale options
238                if app.classic.subplatform == 'cardboard':
239                    rawval = bui.app.config.resolve('GVR Render Target Scale')
240                    current_res_cardboard = (
241                        str(min(100, max(10, int(round(rawval * 100.0))))) + '%'
242                    )
243                    resolution_popup = PopupMenu(
244                        parent=self._root_widget,
245                        position=(h_offs + 60, v - 50),
246                        width=120,
247                        scale=popup_menu_scale,
248                        choices=['100%', '75%', '50%', '35%'],
249                        current_choice=current_res_cardboard,
250                        on_value_change_call=self._set_gvr_render_target_scale,
251                    )
252                else:
253                    native_res = bui.get_display_resolution()
254                    assert native_res is not None
255                    choices = ['Auto', 'Native']
256                    choices_display = [
257                        bui.Lstr(resource='autoText'),
258                        bui.Lstr(resource='nativeText'),
259                    ]
260                    for res in [1440, 1080, 960, 720, 480]:
261                        if native_res[1] >= res:
262                            res_str = f'{res}p'
263                            choices.append(res_str)
264                            choices_display.append(bui.Lstr(value=res_str))
265                    current_res_android = bui.app.config.resolve(
266                        'Resolution (Android)'
267                    )
268                    resolution_popup = PopupMenu(
269                        parent=self._root_widget,
270                        position=(h_offs + 60, v - 50),
271                        width=120,
272                        scale=popup_menu_scale,
273                        choices=choices,
274                        choices_display=choices_display,
275                        current_choice=current_res_android,
276                        on_value_change_call=self._set_android_res,
277                    )
278            else:
279                # If we're on a system that doesn't allow setting resolution,
280                # set pixel-scale instead.
281                current_res = bui.get_display_resolution()
282                if current_res is None:
283                    rawval = bui.app.config.resolve('Screen Pixel Scale')
284                    current_res2 = (
285                        str(min(100, max(10, int(round(rawval * 100.0))))) + '%'
286                    )
287                    resolution_popup = PopupMenu(
288                        parent=self._root_widget,
289                        position=(h_offs + 60, v - 50),
290                        width=120,
291                        scale=popup_menu_scale,
292                        choices=['100%', '88%', '75%', '63%', '50%'],
293                        current_choice=current_res2,
294                        on_value_change_call=self._set_pixel_scale,
295                    )
296                else:
297                    raise RuntimeError(
298                        'obsolete code path; discrete resolutions'
299                        ' no longer supported'
300                    )
301        if resolution_popup is not None:
302            bui.widget(
303                edit=resolution_popup.get_button(),
304                left_widget=back_button,
305            )
306
307        vsync_popup: PopupMenu | None = None
308        if show_vsync:
309            bui.textwidget(
310                parent=self._root_widget,
311                position=(230, v),
312                size=(160, 25),
313                text=bui.Lstr(resource=f'{self._r}.verticalSyncText'),
314                color=bui.app.ui_v1.heading_color,
315                scale=0.65,
316                maxwidth=150,
317                h_align='center',
318                v_align='center',
319            )
320            vsync_popup = PopupMenu(
321                parent=self._root_widget,
322                position=(230, v - 50),
323                width=150,
324                scale=popup_menu_scale,
325                choices=['Auto', 'Always', 'Never'],
326                choices_display=[
327                    bui.Lstr(resource='autoText'),
328                    bui.Lstr(resource=f'{self._r}.alwaysText'),
329                    bui.Lstr(resource=f'{self._r}.neverText'),
330                ],
331                current_choice=bui.app.config.resolve('Vertical Sync'),
332                on_value_change_call=self._set_vsync,
333            )
334            if resolution_popup is not None:
335                bui.widget(
336                    edit=vsync_popup.get_button(),
337                    left_widget=resolution_popup.get_button(),
338                )
339
340        if resolution_popup is not None and vsync_popup is not None:
341            bui.widget(
342                edit=resolution_popup.get_button(),
343                right_widget=vsync_popup.get_button(),
344            )
345
346        v -= 90
347        self._max_fps_text: bui.Widget | None = None
348        if show_max_fps:
349            v -= 5
350            bui.textwidget(
351                parent=self._root_widget,
352                position=(155, v + 10),
353                size=(0, 0),
354                text=bui.Lstr(resource=f'{self._r}.maxFPSText'),
355                color=bui.app.ui_v1.heading_color,
356                scale=0.9,
357                maxwidth=90,
358                h_align='right',
359                v_align='center',
360            )
361
362            max_fps_str = str(bui.app.config.resolve('Max FPS'))
363            self._last_max_fps_str = max_fps_str
364            self._max_fps_text = bui.textwidget(
365                parent=self._root_widget,
366                position=(170, v - 5),
367                size=(105, 30),
368                text=max_fps_str,
369                max_chars=5,
370                editable=True,
371                h_align='left',
372                v_align='center',
373                on_return_press_call=self._on_max_fps_return_press,
374            )
375            v -= 45
376
377        if self._max_fps_text is not None and resolution_popup is not None:
378            bui.widget(
379                edit=resolution_popup.get_button(),
380                down_widget=self._max_fps_text,
381            )
382            bui.widget(
383                edit=self._max_fps_text,
384                up_widget=resolution_popup.get_button(),
385            )
386
387        fpsc = ConfigCheckBox(
388            parent=self._root_widget,
389            position=(69, v - 6),
390            size=(210, 30),
391            scale=0.86,
392            configkey='Show FPS',
393            displayname=bui.Lstr(resource=f'{self._r}.showFPSText'),
394            maxwidth=130,
395        )
396        if self._max_fps_text is not None:
397            bui.widget(
398                edit=self._max_fps_text,
399                down_widget=fpsc.widget,
400            )
401            bui.widget(
402                edit=fpsc.widget,
403                up_widget=self._max_fps_text,
404            )
405
406        if show_tv_mode:
407            tvc = ConfigCheckBox(
408                parent=self._root_widget,
409                position=(240, v - 6),
410                size=(210, 30),
411                scale=0.86,
412                configkey='TV Border',
413                displayname=bui.Lstr(resource=f'{self._r}.tvBorderText'),
414                maxwidth=130,
415            )
416            bui.widget(edit=fpsc.widget, right_widget=tvc.widget)
417            bui.widget(edit=tvc.widget, left_widget=fpsc.widget)
418
419        v -= spacing
420
421        # Make a timer to update our controls in case the config changes
422        # under us.
423        self._update_timer = bui.AppTimer(
424            0.25, bui.WeakCall(self._update_controls), repeat=True
425        )
426
427    @override
428    def get_main_window_state(self) -> bui.MainWindowState:
429        # Support recreating our window for back/refresh purposes.
430        cls = type(self)
431        return bui.BasicMainWindowState(
432            create_call=lambda transition, origin_widget: cls(
433                transition=transition, origin_widget=origin_widget
434            )
435        )
436
437    @override
438    def on_main_window_close(self) -> None:
439        self._apply_max_fps()
440
441    def _set_quality(self, quality: str) -> None:
442        cfg = bui.app.config
443        cfg['Graphics Quality'] = quality
444        cfg.apply_and_commit()
445
446    def _set_textures(self, val: str) -> None:
447        cfg = bui.app.config
448        cfg['Texture Quality'] = val
449        cfg.apply_and_commit()
450
451    def _set_android_res(self, val: str) -> None:
452        cfg = bui.app.config
453        cfg['Resolution (Android)'] = val
454        cfg.apply_and_commit()
455
456    def _set_pixel_scale(self, res: str) -> None:
457        cfg = bui.app.config
458        cfg['Screen Pixel Scale'] = float(res[:-1]) / 100.0
459        cfg.apply_and_commit()
460
461    def _set_gvr_render_target_scale(self, res: str) -> None:
462        cfg = bui.app.config
463        cfg['GVR Render Target Scale'] = float(res[:-1]) / 100.0
464        cfg.apply_and_commit()
465
466    def _set_vsync(self, val: str) -> None:
467        cfg = bui.app.config
468        cfg['Vertical Sync'] = val
469        cfg.apply_and_commit()
470
471    def _on_max_fps_return_press(self) -> None:
472        self._apply_max_fps()
473        bui.containerwidget(
474            edit=self._root_widget, selected_child=cast(bui.Widget, 0)
475        )
476
477    def _apply_max_fps(self) -> None:
478        if not self._max_fps_dirty or not self._max_fps_text:
479            return
480
481        val: Any = bui.textwidget(query=self._max_fps_text)
482        assert isinstance(val, str)
483        # If there's a broken value, replace it with the default.
484        try:
485            ival = int(val)
486        except ValueError:
487            ival = bui.app.config.default_value('Max FPS')
488        assert isinstance(ival, int)
489
490        # Clamp to reasonable limits (allow -1 to mean no max).
491        if ival != -1:
492            ival = max(10, ival)
493            ival = min(99999, ival)
494
495        # Store it to the config.
496        cfg = bui.app.config
497        cfg['Max FPS'] = ival
498        cfg.apply_and_commit()
499
500        # Update the display if we changed the value.
501        if str(ival) != val:
502            bui.textwidget(edit=self._max_fps_text, text=str(ival))
503
504        self._max_fps_dirty = False
505
506    def _update_controls(self) -> None:
507        if self._max_fps_text is not None:
508            # Keep track of when the max-fps value changes. Once it
509            # remains stable for a few moments, apply it.
510            val: Any = bui.textwidget(query=self._max_fps_text)
511            assert isinstance(val, str)
512            if val != self._last_max_fps_str:
513                # Oop; it changed. Note the time and the fact that we'll
514                # need to apply it at some point.
515                self._max_fps_dirty = True
516                self._last_max_fps_str = val
517                self._last_max_fps_set_time = bui.apptime()
518            else:
519                # If its been stable long enough, apply it.
520                if (
521                    self._max_fps_dirty
522                    and bui.apptime() - self._last_max_fps_set_time > 1.0
523                ):
524                    self._apply_max_fps()
525
526        if self._show_fullscreen:
527            # Keep the fullscreen checkbox up to date with the current value.
528            bui.checkboxwidget(
529                edit=self._fullscreen_checkbox,
530                value=bui.fullscreen_control_get(),
531            )
class GraphicsSettingsWindow(bauiv1._uitypes.MainWindow):
 18class GraphicsSettingsWindow(bui.MainWindow):
 19    """Window for graphics settings."""
 20
 21    def __init__(
 22        self,
 23        transition: str | None = 'in_right',
 24        origin_widget: bui.Widget | None = None,
 25    ):
 26        # pylint: disable=too-many-locals
 27        # pylint: disable=too-many-branches
 28        # pylint: disable=too-many-statements
 29
 30        self._r = 'graphicsSettingsWindow'
 31        app = bui.app
 32        assert app.classic is not None
 33
 34        spacing = 32
 35        self._have_selected_child = False
 36        uiscale = app.ui_v1.uiscale
 37        width = 450.0
 38        height = 302.0
 39        self._max_fps_dirty = False
 40        self._last_max_fps_set_time = bui.apptime()
 41        self._last_max_fps_str = ''
 42
 43        self._show_fullscreen = False
 44        fullscreen_spacing_top = spacing * 0.2
 45        fullscreen_spacing = spacing * 1.2
 46        if bui.fullscreen_control_available():
 47            self._show_fullscreen = True
 48            height += fullscreen_spacing + fullscreen_spacing_top
 49
 50        show_vsync = bui.supports_vsync()
 51        show_tv_mode = not bui.app.env.vr
 52
 53        show_max_fps = bui.supports_max_fps()
 54        if show_max_fps:
 55            height += 50
 56
 57        show_resolution = True
 58        if app.env.vr:
 59            show_resolution = (
 60                app.classic.platform == 'android'
 61                and app.classic.subplatform == 'cardboard'
 62            )
 63
 64        assert bui.app.classic is not None
 65        uiscale = bui.app.ui_v1.uiscale
 66        base_scale = (
 67            1.5
 68            if uiscale is bui.UIScale.SMALL
 69            else 1.3 if uiscale is bui.UIScale.MEDIUM else 1.0
 70        )
 71        popup_menu_scale = base_scale * 1.2
 72        v = height - 50
 73        v -= spacing * 1.15
 74        super().__init__(
 75            root_widget=bui.containerwidget(
 76                size=(width, height),
 77                scale=base_scale,
 78                stack_offset=(
 79                    (0, -10) if uiscale is bui.UIScale.SMALL else (0, 0)
 80                ),
 81                toolbar_visibility=(
 82                    None if uiscale is bui.UIScale.SMALL else 'menu_full'
 83                ),
 84            ),
 85            transition=transition,
 86            origin_widget=origin_widget,
 87        )
 88
 89        back_button = bui.buttonwidget(
 90            parent=self._root_widget,
 91            position=(35, height - 50),
 92            size=(60, 60),
 93            scale=0.8,
 94            text_scale=1.2,
 95            autoselect=True,
 96            label=bui.charstr(bui.SpecialChar.BACK),
 97            button_type='backSmall',
 98            on_activate_call=self.main_window_back,
 99        )
100
101        bui.containerwidget(edit=self._root_widget, cancel_button=back_button)
102
103        bui.textwidget(
104            parent=self._root_widget,
105            position=(0, height - 44),
106            size=(width, 25),
107            text=bui.Lstr(resource=f'{self._r}.titleText'),
108            color=bui.app.ui_v1.title_color,
109            h_align='center',
110            v_align='top',
111        )
112
113        self._fullscreen_checkbox: bui.Widget | None = None
114        if self._show_fullscreen:
115            v -= fullscreen_spacing_top
116            # Fullscreen control does not necessarily talk to the
117            # app config so we have to wrangle it manually instead of
118            # using a config-checkbox.
119            label = bui.Lstr(resource=f'{self._r}.fullScreenText')
120
121            # Show keyboard shortcut alongside the control if they
122            # provide one.
123            shortcut = bui.fullscreen_control_key_shortcut()
124            if shortcut is not None:
125                label = bui.Lstr(
126                    value='$(NAME) [$(SHORTCUT)]',
127                    subs=[('$(NAME)', label), ('$(SHORTCUT)', shortcut)],
128                )
129            self._fullscreen_checkbox = bui.checkboxwidget(
130                parent=self._root_widget,
131                position=(100, v),
132                value=bui.fullscreen_control_get(),
133                on_value_change_call=bui.fullscreen_control_set,
134                maxwidth=250,
135                size=(300, 30),
136                text=label,
137            )
138
139            if not self._have_selected_child:
140                bui.containerwidget(
141                    edit=self._root_widget,
142                    selected_child=self._fullscreen_checkbox,
143                )
144                self._have_selected_child = True
145            v -= fullscreen_spacing
146
147        self._selected_color = (0.5, 1, 0.5, 1)
148        self._unselected_color = (0.7, 0.7, 0.7, 1)
149
150        # Quality
151        bui.textwidget(
152            parent=self._root_widget,
153            position=(60, v),
154            size=(160, 25),
155            text=bui.Lstr(resource=f'{self._r}.visualsText'),
156            color=bui.app.ui_v1.heading_color,
157            scale=0.65,
158            maxwidth=150,
159            h_align='center',
160            v_align='center',
161        )
162        PopupMenu(
163            parent=self._root_widget,
164            position=(60, v - 50),
165            width=150,
166            scale=popup_menu_scale,
167            choices=['Auto', 'Higher', 'High', 'Medium', 'Low'],
168            choices_disabled=(
169                ['Higher', 'High']
170                if bui.get_max_graphics_quality() == 'Medium'
171                else []
172            ),
173            choices_display=[
174                bui.Lstr(resource='autoText'),
175                bui.Lstr(resource=f'{self._r}.higherText'),
176                bui.Lstr(resource=f'{self._r}.highText'),
177                bui.Lstr(resource=f'{self._r}.mediumText'),
178                bui.Lstr(resource=f'{self._r}.lowText'),
179            ],
180            current_choice=bui.app.config.resolve('Graphics Quality'),
181            on_value_change_call=self._set_quality,
182        )
183
184        # Texture controls
185        bui.textwidget(
186            parent=self._root_widget,
187            position=(230, v),
188            size=(160, 25),
189            text=bui.Lstr(resource=f'{self._r}.texturesText'),
190            color=bui.app.ui_v1.heading_color,
191            scale=0.65,
192            maxwidth=150,
193            h_align='center',
194            v_align='center',
195        )
196        textures_popup = PopupMenu(
197            parent=self._root_widget,
198            position=(230, v - 50),
199            width=150,
200            scale=popup_menu_scale,
201            choices=['Auto', 'High', 'Medium', 'Low'],
202            choices_display=[
203                bui.Lstr(resource='autoText'),
204                bui.Lstr(resource=f'{self._r}.highText'),
205                bui.Lstr(resource=f'{self._r}.mediumText'),
206                bui.Lstr(resource=f'{self._r}.lowText'),
207            ],
208            current_choice=bui.app.config.resolve('Texture Quality'),
209            on_value_change_call=self._set_textures,
210        )
211        bui.widget(
212            edit=textures_popup.get_button(),
213            right_widget=bui.get_special_widget('squad_button'),
214        )
215        v -= 80
216
217        h_offs = 0
218
219        resolution_popup: PopupMenu | None = None
220
221        if show_resolution:
222            bui.textwidget(
223                parent=self._root_widget,
224                position=(h_offs + 60, v),
225                size=(160, 25),
226                text=bui.Lstr(resource=f'{self._r}.resolutionText'),
227                color=bui.app.ui_v1.heading_color,
228                scale=0.65,
229                maxwidth=150,
230                h_align='center',
231                v_align='center',
232            )
233
234            # On standard android we have 'Auto', 'Native', and a few
235            # HD standards.
236            if app.classic.platform == 'android':
237                # on cardboard/daydream android we have a few
238                # render-target-scale options
239                if app.classic.subplatform == 'cardboard':
240                    rawval = bui.app.config.resolve('GVR Render Target Scale')
241                    current_res_cardboard = (
242                        str(min(100, max(10, int(round(rawval * 100.0))))) + '%'
243                    )
244                    resolution_popup = PopupMenu(
245                        parent=self._root_widget,
246                        position=(h_offs + 60, v - 50),
247                        width=120,
248                        scale=popup_menu_scale,
249                        choices=['100%', '75%', '50%', '35%'],
250                        current_choice=current_res_cardboard,
251                        on_value_change_call=self._set_gvr_render_target_scale,
252                    )
253                else:
254                    native_res = bui.get_display_resolution()
255                    assert native_res is not None
256                    choices = ['Auto', 'Native']
257                    choices_display = [
258                        bui.Lstr(resource='autoText'),
259                        bui.Lstr(resource='nativeText'),
260                    ]
261                    for res in [1440, 1080, 960, 720, 480]:
262                        if native_res[1] >= res:
263                            res_str = f'{res}p'
264                            choices.append(res_str)
265                            choices_display.append(bui.Lstr(value=res_str))
266                    current_res_android = bui.app.config.resolve(
267                        'Resolution (Android)'
268                    )
269                    resolution_popup = PopupMenu(
270                        parent=self._root_widget,
271                        position=(h_offs + 60, v - 50),
272                        width=120,
273                        scale=popup_menu_scale,
274                        choices=choices,
275                        choices_display=choices_display,
276                        current_choice=current_res_android,
277                        on_value_change_call=self._set_android_res,
278                    )
279            else:
280                # If we're on a system that doesn't allow setting resolution,
281                # set pixel-scale instead.
282                current_res = bui.get_display_resolution()
283                if current_res is None:
284                    rawval = bui.app.config.resolve('Screen Pixel Scale')
285                    current_res2 = (
286                        str(min(100, max(10, int(round(rawval * 100.0))))) + '%'
287                    )
288                    resolution_popup = PopupMenu(
289                        parent=self._root_widget,
290                        position=(h_offs + 60, v - 50),
291                        width=120,
292                        scale=popup_menu_scale,
293                        choices=['100%', '88%', '75%', '63%', '50%'],
294                        current_choice=current_res2,
295                        on_value_change_call=self._set_pixel_scale,
296                    )
297                else:
298                    raise RuntimeError(
299                        'obsolete code path; discrete resolutions'
300                        ' no longer supported'
301                    )
302        if resolution_popup is not None:
303            bui.widget(
304                edit=resolution_popup.get_button(),
305                left_widget=back_button,
306            )
307
308        vsync_popup: PopupMenu | None = None
309        if show_vsync:
310            bui.textwidget(
311                parent=self._root_widget,
312                position=(230, v),
313                size=(160, 25),
314                text=bui.Lstr(resource=f'{self._r}.verticalSyncText'),
315                color=bui.app.ui_v1.heading_color,
316                scale=0.65,
317                maxwidth=150,
318                h_align='center',
319                v_align='center',
320            )
321            vsync_popup = PopupMenu(
322                parent=self._root_widget,
323                position=(230, v - 50),
324                width=150,
325                scale=popup_menu_scale,
326                choices=['Auto', 'Always', 'Never'],
327                choices_display=[
328                    bui.Lstr(resource='autoText'),
329                    bui.Lstr(resource=f'{self._r}.alwaysText'),
330                    bui.Lstr(resource=f'{self._r}.neverText'),
331                ],
332                current_choice=bui.app.config.resolve('Vertical Sync'),
333                on_value_change_call=self._set_vsync,
334            )
335            if resolution_popup is not None:
336                bui.widget(
337                    edit=vsync_popup.get_button(),
338                    left_widget=resolution_popup.get_button(),
339                )
340
341        if resolution_popup is not None and vsync_popup is not None:
342            bui.widget(
343                edit=resolution_popup.get_button(),
344                right_widget=vsync_popup.get_button(),
345            )
346
347        v -= 90
348        self._max_fps_text: bui.Widget | None = None
349        if show_max_fps:
350            v -= 5
351            bui.textwidget(
352                parent=self._root_widget,
353                position=(155, v + 10),
354                size=(0, 0),
355                text=bui.Lstr(resource=f'{self._r}.maxFPSText'),
356                color=bui.app.ui_v1.heading_color,
357                scale=0.9,
358                maxwidth=90,
359                h_align='right',
360                v_align='center',
361            )
362
363            max_fps_str = str(bui.app.config.resolve('Max FPS'))
364            self._last_max_fps_str = max_fps_str
365            self._max_fps_text = bui.textwidget(
366                parent=self._root_widget,
367                position=(170, v - 5),
368                size=(105, 30),
369                text=max_fps_str,
370                max_chars=5,
371                editable=True,
372                h_align='left',
373                v_align='center',
374                on_return_press_call=self._on_max_fps_return_press,
375            )
376            v -= 45
377
378        if self._max_fps_text is not None and resolution_popup is not None:
379            bui.widget(
380                edit=resolution_popup.get_button(),
381                down_widget=self._max_fps_text,
382            )
383            bui.widget(
384                edit=self._max_fps_text,
385                up_widget=resolution_popup.get_button(),
386            )
387
388        fpsc = ConfigCheckBox(
389            parent=self._root_widget,
390            position=(69, v - 6),
391            size=(210, 30),
392            scale=0.86,
393            configkey='Show FPS',
394            displayname=bui.Lstr(resource=f'{self._r}.showFPSText'),
395            maxwidth=130,
396        )
397        if self._max_fps_text is not None:
398            bui.widget(
399                edit=self._max_fps_text,
400                down_widget=fpsc.widget,
401            )
402            bui.widget(
403                edit=fpsc.widget,
404                up_widget=self._max_fps_text,
405            )
406
407        if show_tv_mode:
408            tvc = ConfigCheckBox(
409                parent=self._root_widget,
410                position=(240, v - 6),
411                size=(210, 30),
412                scale=0.86,
413                configkey='TV Border',
414                displayname=bui.Lstr(resource=f'{self._r}.tvBorderText'),
415                maxwidth=130,
416            )
417            bui.widget(edit=fpsc.widget, right_widget=tvc.widget)
418            bui.widget(edit=tvc.widget, left_widget=fpsc.widget)
419
420        v -= spacing
421
422        # Make a timer to update our controls in case the config changes
423        # under us.
424        self._update_timer = bui.AppTimer(
425            0.25, bui.WeakCall(self._update_controls), repeat=True
426        )
427
428    @override
429    def get_main_window_state(self) -> bui.MainWindowState:
430        # Support recreating our window for back/refresh purposes.
431        cls = type(self)
432        return bui.BasicMainWindowState(
433            create_call=lambda transition, origin_widget: cls(
434                transition=transition, origin_widget=origin_widget
435            )
436        )
437
438    @override
439    def on_main_window_close(self) -> None:
440        self._apply_max_fps()
441
442    def _set_quality(self, quality: str) -> None:
443        cfg = bui.app.config
444        cfg['Graphics Quality'] = quality
445        cfg.apply_and_commit()
446
447    def _set_textures(self, val: str) -> None:
448        cfg = bui.app.config
449        cfg['Texture Quality'] = val
450        cfg.apply_and_commit()
451
452    def _set_android_res(self, val: str) -> None:
453        cfg = bui.app.config
454        cfg['Resolution (Android)'] = val
455        cfg.apply_and_commit()
456
457    def _set_pixel_scale(self, res: str) -> None:
458        cfg = bui.app.config
459        cfg['Screen Pixel Scale'] = float(res[:-1]) / 100.0
460        cfg.apply_and_commit()
461
462    def _set_gvr_render_target_scale(self, res: str) -> None:
463        cfg = bui.app.config
464        cfg['GVR Render Target Scale'] = float(res[:-1]) / 100.0
465        cfg.apply_and_commit()
466
467    def _set_vsync(self, val: str) -> None:
468        cfg = bui.app.config
469        cfg['Vertical Sync'] = val
470        cfg.apply_and_commit()
471
472    def _on_max_fps_return_press(self) -> None:
473        self._apply_max_fps()
474        bui.containerwidget(
475            edit=self._root_widget, selected_child=cast(bui.Widget, 0)
476        )
477
478    def _apply_max_fps(self) -> None:
479        if not self._max_fps_dirty or not self._max_fps_text:
480            return
481
482        val: Any = bui.textwidget(query=self._max_fps_text)
483        assert isinstance(val, str)
484        # If there's a broken value, replace it with the default.
485        try:
486            ival = int(val)
487        except ValueError:
488            ival = bui.app.config.default_value('Max FPS')
489        assert isinstance(ival, int)
490
491        # Clamp to reasonable limits (allow -1 to mean no max).
492        if ival != -1:
493            ival = max(10, ival)
494            ival = min(99999, ival)
495
496        # Store it to the config.
497        cfg = bui.app.config
498        cfg['Max FPS'] = ival
499        cfg.apply_and_commit()
500
501        # Update the display if we changed the value.
502        if str(ival) != val:
503            bui.textwidget(edit=self._max_fps_text, text=str(ival))
504
505        self._max_fps_dirty = False
506
507    def _update_controls(self) -> None:
508        if self._max_fps_text is not None:
509            # Keep track of when the max-fps value changes. Once it
510            # remains stable for a few moments, apply it.
511            val: Any = bui.textwidget(query=self._max_fps_text)
512            assert isinstance(val, str)
513            if val != self._last_max_fps_str:
514                # Oop; it changed. Note the time and the fact that we'll
515                # need to apply it at some point.
516                self._max_fps_dirty = True
517                self._last_max_fps_str = val
518                self._last_max_fps_set_time = bui.apptime()
519            else:
520                # If its been stable long enough, apply it.
521                if (
522                    self._max_fps_dirty
523                    and bui.apptime() - self._last_max_fps_set_time > 1.0
524                ):
525                    self._apply_max_fps()
526
527        if self._show_fullscreen:
528            # Keep the fullscreen checkbox up to date with the current value.
529            bui.checkboxwidget(
530                edit=self._fullscreen_checkbox,
531                value=bui.fullscreen_control_get(),
532            )

Window for graphics settings.

GraphicsSettingsWindow( 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        # pylint: disable=too-many-locals
 27        # pylint: disable=too-many-branches
 28        # pylint: disable=too-many-statements
 29
 30        self._r = 'graphicsSettingsWindow'
 31        app = bui.app
 32        assert app.classic is not None
 33
 34        spacing = 32
 35        self._have_selected_child = False
 36        uiscale = app.ui_v1.uiscale
 37        width = 450.0
 38        height = 302.0
 39        self._max_fps_dirty = False
 40        self._last_max_fps_set_time = bui.apptime()
 41        self._last_max_fps_str = ''
 42
 43        self._show_fullscreen = False
 44        fullscreen_spacing_top = spacing * 0.2
 45        fullscreen_spacing = spacing * 1.2
 46        if bui.fullscreen_control_available():
 47            self._show_fullscreen = True
 48            height += fullscreen_spacing + fullscreen_spacing_top
 49
 50        show_vsync = bui.supports_vsync()
 51        show_tv_mode = not bui.app.env.vr
 52
 53        show_max_fps = bui.supports_max_fps()
 54        if show_max_fps:
 55            height += 50
 56
 57        show_resolution = True
 58        if app.env.vr:
 59            show_resolution = (
 60                app.classic.platform == 'android'
 61                and app.classic.subplatform == 'cardboard'
 62            )
 63
 64        assert bui.app.classic is not None
 65        uiscale = bui.app.ui_v1.uiscale
 66        base_scale = (
 67            1.5
 68            if uiscale is bui.UIScale.SMALL
 69            else 1.3 if uiscale is bui.UIScale.MEDIUM else 1.0
 70        )
 71        popup_menu_scale = base_scale * 1.2
 72        v = height - 50
 73        v -= spacing * 1.15
 74        super().__init__(
 75            root_widget=bui.containerwidget(
 76                size=(width, height),
 77                scale=base_scale,
 78                stack_offset=(
 79                    (0, -10) if uiscale is bui.UIScale.SMALL else (0, 0)
 80                ),
 81                toolbar_visibility=(
 82                    None if uiscale is bui.UIScale.SMALL else 'menu_full'
 83                ),
 84            ),
 85            transition=transition,
 86            origin_widget=origin_widget,
 87        )
 88
 89        back_button = bui.buttonwidget(
 90            parent=self._root_widget,
 91            position=(35, height - 50),
 92            size=(60, 60),
 93            scale=0.8,
 94            text_scale=1.2,
 95            autoselect=True,
 96            label=bui.charstr(bui.SpecialChar.BACK),
 97            button_type='backSmall',
 98            on_activate_call=self.main_window_back,
 99        )
100
101        bui.containerwidget(edit=self._root_widget, cancel_button=back_button)
102
103        bui.textwidget(
104            parent=self._root_widget,
105            position=(0, height - 44),
106            size=(width, 25),
107            text=bui.Lstr(resource=f'{self._r}.titleText'),
108            color=bui.app.ui_v1.title_color,
109            h_align='center',
110            v_align='top',
111        )
112
113        self._fullscreen_checkbox: bui.Widget | None = None
114        if self._show_fullscreen:
115            v -= fullscreen_spacing_top
116            # Fullscreen control does not necessarily talk to the
117            # app config so we have to wrangle it manually instead of
118            # using a config-checkbox.
119            label = bui.Lstr(resource=f'{self._r}.fullScreenText')
120
121            # Show keyboard shortcut alongside the control if they
122            # provide one.
123            shortcut = bui.fullscreen_control_key_shortcut()
124            if shortcut is not None:
125                label = bui.Lstr(
126                    value='$(NAME) [$(SHORTCUT)]',
127                    subs=[('$(NAME)', label), ('$(SHORTCUT)', shortcut)],
128                )
129            self._fullscreen_checkbox = bui.checkboxwidget(
130                parent=self._root_widget,
131                position=(100, v),
132                value=bui.fullscreen_control_get(),
133                on_value_change_call=bui.fullscreen_control_set,
134                maxwidth=250,
135                size=(300, 30),
136                text=label,
137            )
138
139            if not self._have_selected_child:
140                bui.containerwidget(
141                    edit=self._root_widget,
142                    selected_child=self._fullscreen_checkbox,
143                )
144                self._have_selected_child = True
145            v -= fullscreen_spacing
146
147        self._selected_color = (0.5, 1, 0.5, 1)
148        self._unselected_color = (0.7, 0.7, 0.7, 1)
149
150        # Quality
151        bui.textwidget(
152            parent=self._root_widget,
153            position=(60, v),
154            size=(160, 25),
155            text=bui.Lstr(resource=f'{self._r}.visualsText'),
156            color=bui.app.ui_v1.heading_color,
157            scale=0.65,
158            maxwidth=150,
159            h_align='center',
160            v_align='center',
161        )
162        PopupMenu(
163            parent=self._root_widget,
164            position=(60, v - 50),
165            width=150,
166            scale=popup_menu_scale,
167            choices=['Auto', 'Higher', 'High', 'Medium', 'Low'],
168            choices_disabled=(
169                ['Higher', 'High']
170                if bui.get_max_graphics_quality() == 'Medium'
171                else []
172            ),
173            choices_display=[
174                bui.Lstr(resource='autoText'),
175                bui.Lstr(resource=f'{self._r}.higherText'),
176                bui.Lstr(resource=f'{self._r}.highText'),
177                bui.Lstr(resource=f'{self._r}.mediumText'),
178                bui.Lstr(resource=f'{self._r}.lowText'),
179            ],
180            current_choice=bui.app.config.resolve('Graphics Quality'),
181            on_value_change_call=self._set_quality,
182        )
183
184        # Texture controls
185        bui.textwidget(
186            parent=self._root_widget,
187            position=(230, v),
188            size=(160, 25),
189            text=bui.Lstr(resource=f'{self._r}.texturesText'),
190            color=bui.app.ui_v1.heading_color,
191            scale=0.65,
192            maxwidth=150,
193            h_align='center',
194            v_align='center',
195        )
196        textures_popup = PopupMenu(
197            parent=self._root_widget,
198            position=(230, v - 50),
199            width=150,
200            scale=popup_menu_scale,
201            choices=['Auto', 'High', 'Medium', 'Low'],
202            choices_display=[
203                bui.Lstr(resource='autoText'),
204                bui.Lstr(resource=f'{self._r}.highText'),
205                bui.Lstr(resource=f'{self._r}.mediumText'),
206                bui.Lstr(resource=f'{self._r}.lowText'),
207            ],
208            current_choice=bui.app.config.resolve('Texture Quality'),
209            on_value_change_call=self._set_textures,
210        )
211        bui.widget(
212            edit=textures_popup.get_button(),
213            right_widget=bui.get_special_widget('squad_button'),
214        )
215        v -= 80
216
217        h_offs = 0
218
219        resolution_popup: PopupMenu | None = None
220
221        if show_resolution:
222            bui.textwidget(
223                parent=self._root_widget,
224                position=(h_offs + 60, v),
225                size=(160, 25),
226                text=bui.Lstr(resource=f'{self._r}.resolutionText'),
227                color=bui.app.ui_v1.heading_color,
228                scale=0.65,
229                maxwidth=150,
230                h_align='center',
231                v_align='center',
232            )
233
234            # On standard android we have 'Auto', 'Native', and a few
235            # HD standards.
236            if app.classic.platform == 'android':
237                # on cardboard/daydream android we have a few
238                # render-target-scale options
239                if app.classic.subplatform == 'cardboard':
240                    rawval = bui.app.config.resolve('GVR Render Target Scale')
241                    current_res_cardboard = (
242                        str(min(100, max(10, int(round(rawval * 100.0))))) + '%'
243                    )
244                    resolution_popup = PopupMenu(
245                        parent=self._root_widget,
246                        position=(h_offs + 60, v - 50),
247                        width=120,
248                        scale=popup_menu_scale,
249                        choices=['100%', '75%', '50%', '35%'],
250                        current_choice=current_res_cardboard,
251                        on_value_change_call=self._set_gvr_render_target_scale,
252                    )
253                else:
254                    native_res = bui.get_display_resolution()
255                    assert native_res is not None
256                    choices = ['Auto', 'Native']
257                    choices_display = [
258                        bui.Lstr(resource='autoText'),
259                        bui.Lstr(resource='nativeText'),
260                    ]
261                    for res in [1440, 1080, 960, 720, 480]:
262                        if native_res[1] >= res:
263                            res_str = f'{res}p'
264                            choices.append(res_str)
265                            choices_display.append(bui.Lstr(value=res_str))
266                    current_res_android = bui.app.config.resolve(
267                        'Resolution (Android)'
268                    )
269                    resolution_popup = PopupMenu(
270                        parent=self._root_widget,
271                        position=(h_offs + 60, v - 50),
272                        width=120,
273                        scale=popup_menu_scale,
274                        choices=choices,
275                        choices_display=choices_display,
276                        current_choice=current_res_android,
277                        on_value_change_call=self._set_android_res,
278                    )
279            else:
280                # If we're on a system that doesn't allow setting resolution,
281                # set pixel-scale instead.
282                current_res = bui.get_display_resolution()
283                if current_res is None:
284                    rawval = bui.app.config.resolve('Screen Pixel Scale')
285                    current_res2 = (
286                        str(min(100, max(10, int(round(rawval * 100.0))))) + '%'
287                    )
288                    resolution_popup = PopupMenu(
289                        parent=self._root_widget,
290                        position=(h_offs + 60, v - 50),
291                        width=120,
292                        scale=popup_menu_scale,
293                        choices=['100%', '88%', '75%', '63%', '50%'],
294                        current_choice=current_res2,
295                        on_value_change_call=self._set_pixel_scale,
296                    )
297                else:
298                    raise RuntimeError(
299                        'obsolete code path; discrete resolutions'
300                        ' no longer supported'
301                    )
302        if resolution_popup is not None:
303            bui.widget(
304                edit=resolution_popup.get_button(),
305                left_widget=back_button,
306            )
307
308        vsync_popup: PopupMenu | None = None
309        if show_vsync:
310            bui.textwidget(
311                parent=self._root_widget,
312                position=(230, v),
313                size=(160, 25),
314                text=bui.Lstr(resource=f'{self._r}.verticalSyncText'),
315                color=bui.app.ui_v1.heading_color,
316                scale=0.65,
317                maxwidth=150,
318                h_align='center',
319                v_align='center',
320            )
321            vsync_popup = PopupMenu(
322                parent=self._root_widget,
323                position=(230, v - 50),
324                width=150,
325                scale=popup_menu_scale,
326                choices=['Auto', 'Always', 'Never'],
327                choices_display=[
328                    bui.Lstr(resource='autoText'),
329                    bui.Lstr(resource=f'{self._r}.alwaysText'),
330                    bui.Lstr(resource=f'{self._r}.neverText'),
331                ],
332                current_choice=bui.app.config.resolve('Vertical Sync'),
333                on_value_change_call=self._set_vsync,
334            )
335            if resolution_popup is not None:
336                bui.widget(
337                    edit=vsync_popup.get_button(),
338                    left_widget=resolution_popup.get_button(),
339                )
340
341        if resolution_popup is not None and vsync_popup is not None:
342            bui.widget(
343                edit=resolution_popup.get_button(),
344                right_widget=vsync_popup.get_button(),
345            )
346
347        v -= 90
348        self._max_fps_text: bui.Widget | None = None
349        if show_max_fps:
350            v -= 5
351            bui.textwidget(
352                parent=self._root_widget,
353                position=(155, v + 10),
354                size=(0, 0),
355                text=bui.Lstr(resource=f'{self._r}.maxFPSText'),
356                color=bui.app.ui_v1.heading_color,
357                scale=0.9,
358                maxwidth=90,
359                h_align='right',
360                v_align='center',
361            )
362
363            max_fps_str = str(bui.app.config.resolve('Max FPS'))
364            self._last_max_fps_str = max_fps_str
365            self._max_fps_text = bui.textwidget(
366                parent=self._root_widget,
367                position=(170, v - 5),
368                size=(105, 30),
369                text=max_fps_str,
370                max_chars=5,
371                editable=True,
372                h_align='left',
373                v_align='center',
374                on_return_press_call=self._on_max_fps_return_press,
375            )
376            v -= 45
377
378        if self._max_fps_text is not None and resolution_popup is not None:
379            bui.widget(
380                edit=resolution_popup.get_button(),
381                down_widget=self._max_fps_text,
382            )
383            bui.widget(
384                edit=self._max_fps_text,
385                up_widget=resolution_popup.get_button(),
386            )
387
388        fpsc = ConfigCheckBox(
389            parent=self._root_widget,
390            position=(69, v - 6),
391            size=(210, 30),
392            scale=0.86,
393            configkey='Show FPS',
394            displayname=bui.Lstr(resource=f'{self._r}.showFPSText'),
395            maxwidth=130,
396        )
397        if self._max_fps_text is not None:
398            bui.widget(
399                edit=self._max_fps_text,
400                down_widget=fpsc.widget,
401            )
402            bui.widget(
403                edit=fpsc.widget,
404                up_widget=self._max_fps_text,
405            )
406
407        if show_tv_mode:
408            tvc = ConfigCheckBox(
409                parent=self._root_widget,
410                position=(240, v - 6),
411                size=(210, 30),
412                scale=0.86,
413                configkey='TV Border',
414                displayname=bui.Lstr(resource=f'{self._r}.tvBorderText'),
415                maxwidth=130,
416            )
417            bui.widget(edit=fpsc.widget, right_widget=tvc.widget)
418            bui.widget(edit=tvc.widget, left_widget=fpsc.widget)
419
420        v -= spacing
421
422        # Make a timer to update our controls in case the config changes
423        # under us.
424        self._update_timer = bui.AppTimer(
425            0.25, bui.WeakCall(self._update_controls), repeat=True
426        )

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 get_main_window_state(self) -> bauiv1.MainWindowState:
428    @override
429    def get_main_window_state(self) -> bui.MainWindowState:
430        # Support recreating our window for back/refresh purposes.
431        cls = type(self)
432        return bui.BasicMainWindowState(
433            create_call=lambda transition, origin_widget: cls(
434                transition=transition, origin_widget=origin_widget
435            )
436        )

Return a WindowState to recreate this window, if supported.

@override
def on_main_window_close(self) -> None:
438    @override
439    def on_main_window_close(self) -> None:
440        self._apply_max_fps()

Called before transitioning out a main window.

A good opportunity to save window state/etc.