Source code for bauiv1lib.cloudui._window

# Released under the MIT License. See LICENSE for details.
#
"""UIs provided by the cloud (similar-ish to html in concept)."""

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING, override

import bauiv1 as bui

from bauiv1lib.utils import scroll_fade_bottom, scroll_fade_top
from bauiv1lib.cloudui._prep import CloudUIPagePrep

if TYPE_CHECKING:
    from typing import Callable

    import bacommon.cloudui.v1
    from bauiv1lib.cloudui._controller import CloudUIController


[docs] class CloudUIWindow(bui.MainWindow): """UI provided by the cloud."""
[docs] @dataclass class State: """Final state window can be set to show.""" controller: CloudUIController page: bacommon.cloudui.v1.Page
def __init__( self, state: State | None, *, transition: str | None = 'in_right', origin_widget: bui.Widget | None = None, auxiliary_style: bool = True, ): ui = bui.app.ui_v1 self._state: CloudUIWindow.State | None = None # We want to display differently whether we're an auxiliary # window or not, but unfortunately that value is not yet # available until we're added to the main-window-stack so it # must be explicitly passed in. self._auxiliary_style = auxiliary_style # Calc scale and size for our backing window. For medium & large # ui-scale we aim for a window small enough to always be fully # visible on-screen and for small mode we aim for a window big # enough that we never see the window edges; only the window # texture covering the whole screen. uiscale = ui.uiscale self._width = ( 1400 if uiscale is bui.UIScale.SMALL else 1100 if uiscale is bui.UIScale.MEDIUM else 1200 ) self._height = ( 1200 if uiscale is bui.UIScale.SMALL else 700 if uiscale is bui.UIScale.MEDIUM else 800 ) self._root_scale = ( 1.5 if uiscale is bui.UIScale.SMALL else 0.9 if uiscale is bui.UIScale.MEDIUM else 0.8 ) # Do some fancy math to calculate our visible area; this will be # limited by the screen size in small mode and our backing size # otherwise. screensize = bui.get_virtual_screen_size() self._vis_width = min( self._width - 150, screensize[0] / self._root_scale ) self._vis_height = min( self._height - 80, screensize[1] / self._root_scale ) self._vis_top = 0.5 * self._height + 0.5 * self._vis_height self._vis_left = 0.5 * self._width - 0.5 * self._vis_width self._scroll_width = self._vis_width self._scroll_left = self._vis_left + 0.5 * ( self._vis_width - self._scroll_width ) # Go with full-screen scrollable aread in small ui. self._scroll_height = self._vis_height - ( -1 if uiscale is bui.UIScale.SMALL else 43 ) self._scroll_bottom = ( self._vis_top - (-1 if uiscale is bui.UIScale.SMALL else 32) - self._scroll_height ) # Nudge our vis area up a bit when we can see the full backing # (visual fudge factor). if uiscale is not bui.UIScale.SMALL: self._vis_top += 12.0 super().__init__( root_widget=bui.containerwidget( size=(self._width, self._height), toolbar_visibility='menu_full', toolbar_cancel_button_style=( 'close' if auxiliary_style else 'back' ), scale=self._root_scale, ), transition=transition, origin_widget=origin_widget, # We respond to screen size changes only at small ui-scale; # in other cases we assume our window remains fully visible # always (flip to windowed mode and resize the app window to # confirm this). refresh_on_screen_size_changes=uiscale is bui.UIScale.SMALL, ) # Avoid complaints if nothing is selected under us. bui.widget(edit=self._root_widget, allow_preserve_selection=False) self._subcontainer: bui.Widget | None = None self._scrollwidget = bui.scrollwidget( parent=self._root_widget, highlight=True, # Will turn off once we have UI. size=(self._scroll_width, self._scroll_height), position=(self._scroll_left, self._scroll_bottom), border_opacity=0.4, center_small_content_horizontally=True, claims_left_right=True, ) # Avoid having to deal with selecting this while its empty. # bui.containerwidget(edit=self._scrollwidget, selectable=False) bui.widget(edit=self._scrollwidget, autoselect=True) # With full-screen scrolling, fade content as it approaches # toolbars. if uiscale is bui.UIScale.SMALL and bool(True): scroll_fade_top( self._root_widget, self._width * 0.5 - self._scroll_width * 0.5, self._scroll_bottom, self._scroll_width, self._scroll_height, ) scroll_fade_bottom( self._root_widget, self._width * 0.5 - self._scroll_width * 0.5, self._scroll_bottom, self._scroll_width, self._scroll_height, ) # Title. self._title = bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._vis_top - 20), size=(0, 0), text='', color=ui.title_color, scale=0.9 if uiscale is bui.UIScale.SMALL else 1.0, # Make sure we avoid overlapping meters in small mode. maxwidth=(130 if uiscale is bui.UIScale.SMALL else 200), h_align='center', v_align='center', ) # Needed to display properly over scrolled content. bui.widget(edit=self._title, depth_range=(0.9, 1.0)) # For small UI-scale we use the system back/close button; # otherwise we make our own. if uiscale is bui.UIScale.SMALL: bui.containerwidget( edit=self._root_widget, on_cancel_call=self.main_window_back ) self._back_button: bui.Widget | None = None else: self._back_button = bui.buttonwidget( parent=self._root_widget, id=f'{self.main_window_id_prefix}|close', scale=0.8, position=(self._vis_left + 2, self._vis_top - 35), size=(50, 50) if auxiliary_style else (60, 55), extra_touch_border_scale=2.0, button_type=None if auxiliary_style else 'backSmall', on_activate_call=self.main_window_back, autoselect=True, label=bui.charstr( bui.SpecialChar.CLOSE if auxiliary_style else bui.SpecialChar.BACK ), ) bui.containerwidget( edit=self._root_widget, cancel_button=self._back_button ) # Show our vis-area bounds (for debugging). if bool(False): # Skip top-left since its always overlapping back/close # buttons. if bool(False): bui.textwidget( parent=self._root_widget, position=(self._vis_left, self._vis_top), size=(0, 0), color=(1, 1, 1, 0.5), scale=0.5, text='TL', h_align='left', v_align='top', ) bui.textwidget( parent=self._root_widget, position=(self._vis_left + self._vis_width, self._vis_top), size=(0, 0), color=(1, 1, 1, 0.5), scale=0.5, text='TR', h_align='right', v_align='top', ) bui.textwidget( parent=self._root_widget, position=(self._vis_left, self._vis_top - self._vis_height), size=(0, 0), color=(1, 1, 1, 0.5), scale=0.5, text='BL', h_align='left', v_align='bottom', ) bui.textwidget( parent=self._root_widget, position=( self._vis_left + self._vis_width, self._vis_top - self._vis_height, ), size=(0, 0), scale=0.5, color=(1, 1, 1, 0.5), text='BR', h_align='right', v_align='bottom', ) self._spinner: bui.Widget | None = bui.spinnerwidget( parent=self._root_widget, position=( self._vis_left + self._vis_width * 0.5, self._vis_top - self._vis_height * 0.5, ), size=48, style='bomb', ) if state is not None: self.set_state(state, immediate=True)
[docs] def set_state(self, state: State, immediate: bool = False) -> None: """Set a final state (error or page contents). This state may be instantly restored if the window is recreated (depending on cache lifespan/etc.) """ assert bui.in_logic_thread() assert self._state is None self._state = state ui = bui.app.ui_v1 uiscale = ui.uiscale if self._spinner: self._spinner.delete() self._spinner = None # Ok; we've got content. bui.textwidget( edit=self._title, literal=not state.page.title_is_lstr, text=state.page.title, ) # Make sure there's at least one row and that all rows contain # at least one button. Otherwise show a 'nothing here' message. if not state.page.rows or not all( row.buttons for row in state.page.rows ): bui.uilog.exception( 'Got invalid cloud-ui state;' ' must contain at least one row' ' and all rows must contain buttons.' ) bui.textwidget( parent=self._root_widget, position=( self._vis_left + 0.5 * self._vis_width, self._vis_top - 0.5 * self._vis_height, ), size=(0, 0), scale=0.6, text=bui.Lstr( translate=('serverResponses', 'There is nothing here.') ), h_align='center', v_align='center', ) return pageprep = CloudUIPagePrep( state.page, uiscale, self._scroll_width, immediate=immediate, idprefix=self.main_window_id_prefix, ) # We left highlighting on so the user could see something if # selecting our empty window, but let's kill it now that we're # no longer empty. bui.scrollwidget(edit=self._scrollwidget, highlight=False) bui.scrollwidget( edit=self._scrollwidget, simple_culling_v=pageprep.simple_culling_v, center_small_content=state.page.center_vertically, ) self._subcontainer = pageprep.instantiate( self._scrollwidget, backbutton=( bui.get_special_widget('back_button') if self._back_button is None else self._back_button ), windowbackbutton=self._back_button, ) # Most of our UI won't exist until this point so we need to # explicitly restore state for selection restore to work. # # Note to self: perhaps we should *not* do this if significant # time has passed since the window was made or if input commands # have happened. self.main_window_restore_shared_state()
[docs] @override def get_main_window_state(self) -> bui.MainWindowState: # Support recreating our window for back/refresh purposes. cls = type(self) # IMPORTANT - Pull values from self HERE; if we do it in the # lambda below it'll keep self alive which will lead to # 'ui-not-getting-cleaned-up' warnings and memory leaks. auxiliary_style = self._auxiliary_style state = self._state return bui.BasicMainWindowState( create_call=lambda transition, origin_widget: cls( state=state, transition=transition, origin_widget=origin_widget, auxiliary_style=auxiliary_style, ), )
[docs] @override def main_window_should_preserve_selection(self) -> bool: return True
[docs] @override def get_main_window_shared_state_id(self) -> str | None: return 'cloudui'
# Docs-generation hack; import some stuff that we likely only forward-declared # in our actual source code so that docs tools can find it. from typing import (Coroutine, Any, Literal, Callable, Generator, Awaitable, Sequence, Self) import asyncio from concurrent.futures import Future from pathlib import Path from enum import Enum