Source code for bauiv1lib.docuitest

# Released under the MIT License. See LICENSE for details.
#
"""Examples/tests for using DocUI to build UIs."""

from __future__ import annotations  # Docs-generation hack.

import time
import copy
from typing import TYPE_CHECKING, override

from efro.error import CleanError
import bauiv1 as bui
from bauiv1 import builtinassets
from bauiv1 import classicassets

from bauiv1lib.docui import DocUIWindow, DocUIController

if TYPE_CHECKING:
    from bacommon.docui import DocUIRequest, DocUIResponse
    import bacommon.docui.v2
    from bacommon.langstr import LangStrSpec

    from bauiv1lib.docui import DocUILocalAction


def _btex(name: str) -> str:
    """Qualified ref for a texture in the builtin asset-package."""
    return f'{builtinassets.__asset_package__}:textures/{name}'


def _stex(name: str) -> str:
    """Qualified classicassets texture ref."""
    return f'{classicassets.__asset_package__}:textures/{name}'


[docs] def show_test_doc_ui_v2_window() -> None: """Bust out a doc-ui test window built locally on the client. Test pages authored as language-agnostic v2 documents — text as ``LangStrSpec`` values from the ``badocuiv2testassets`` package (decoded in the client's locale at render time), textures/meshes as typed refs, and multi-line labels via wrap-params instead of hand-baked newlines. The Cloud-Msg and Web buttons fetch equivalent v2 pages from bamaster, keeping the full cloud/web resolve -> decode -> render paths exercised. """ import bacommon.docui.v2 as dui2 bui.app.ui_v1.auxiliary_window_activate( win_type=DocUIWindow, win_create_call=bui.CallStrict( TestDocUIV2Controller().create_window, dui2.Request('/') ), win_extra_type_id=TestDocUIV2Controller.get_window_extra_type_id(), )
[docs] class TestDocUIV2Controller(DocUIController): """Tests/demonstrations of native (v2 / l-string) docui. Local pages are authored client-side; the ``/cloudmsgtest/*`` and ``/webtest/*`` paths fetch equivalent v2 pages from bamaster. """
[docs] @override def fulfill_request(self, request: DocUIRequest) -> DocUIResponse: """Fulfill a v2 request (called in a background thread).""" # pylint: disable=too-many-return-statements import bacommon.docui.v2 as dui2 if not isinstance(request, dui2.Request): raise CleanError('Invalid request version.') # Handle some pages purely locally. if request.path == '/': return _test_v2_page_root(request) if request.path == '/test2': return _test_v2_page_2(request) if request.path == '/slow': return _test_v2_page_long(request) if request.path == '/timedactions': return _test_v2_page_timed_actions(request) if request.path == '/displayitems': return _test_v2_page_display_items(request) if request.path == '/emptypage': return _test_v2_page_empty(request) if request.path == '/boundstests': return _test_v2_bounds(request) # Ship '/webtest/*' off to some webserver to handle. if request.path.startswith('/webtest/'): return self.fulfill_request_web( request, 'https://www.ballistica.net/docuitest' ) # Ship '/cloudmsgtest/*' through our cloud connection to handle. if request.path.startswith('/cloudmsgtest/'): return self.fulfill_request_cloud(request, 'docuitestv2') raise CleanError('Invalid request path.')
[docs] @override def local_action(self, action: DocUILocalAction) -> None: bui.screenmessage( f'Would do {action.name!r} with args {action.args!r}.' )
def _test_v2_page_root( request: bacommon.docui.v2.Request, ) -> bacommon.docui.v2.Response: """Author the v2 (l-string) test root page purely on the client. The full v1 test root page, with all text authored as language-agnostic ``LangStrSpec`` values from the ``badocuiv2testassets`` package, textures/meshes as typed refs from ``builtinassets``/``classicassets``, and multi-line labels wrapped via definition-time :class:`~bacommon.langstr.WrapParams` on the package's string definitions (decision D-t) instead of v1's hand-baked newlines. The client resolves the referenced packages in its own locale, decodes, and wraps -- so this single response renders in any language. """ import bacommon.clienteffect as clfx import bacommon.docui.v2 as dui2 from bauiv1 import _docuiv2testassets strs = _docuiv2testassets.strings # Show some specific debug bits if they ask us to. debug = bool(request.args.get('debug', False)) response = dui2.Response( page=dui2.Page( title=strs.nav.test_root_title.spec, rows=[ dui2.ButtonRow( debug=debug, header_height=100, header_decorations_left=[ dui2.Text( text=strs.common.header_left.spec, position=(0, 10 + 20), color=(1, 1, 1, 0.3), size=(150, 30), h_align=dui2.HAlign.LEFT, debug=debug, ), ], header_decorations_center=[ dui2.Text( text=strs.common.hello_from_docui.spec, position=(0, 10 + 20), size=(300, 30), debug=debug, ), dui2.Text( text=strs.common.docui_reference.spec, scale=0.5, position=(0, -18 + 20), size=(600, 23), debug=debug, ), dui2.Image( texture=builtinassets.textures.nub, position=(0, -58 + 20), size=(60, 60), ), ], header_decorations_right=[ dui2.Text( text=strs.common.header_right.spec, position=(0, 10 + 20), color=(1, 1, 1, 0.3), size=(150, 30), h_align=dui2.HAlign.RIGHT, debug=debug, ), ], title=strs.nav.some_tests.spec, buttons=[ dui2.Button( label=strs.nav.browse.spec, size=(120, 80), action=dui2.Browse(dui2.Request('/test2')), ), dui2.Button( label=strs.nav.replace.spec, size=(120, 80), action=dui2.Replace(dui2.Request('/test2')), ), dui2.Button( label=strs.nav.close.spec, size=(120, 80), action=dui2.Local(close_window=True), ), dui2.Button( label=strs.common.invalid_request.spec, size=(120, 80), action=dui2.Browse(dui2.Request('/invalidrequest')), ), dui2.Button( label=strs.effects.immediate_client_effects.spec, size=(120, 80), action=dui2.Local( # V2 effect forms: l-string text decoded # in the client's locale + a typed sound # ref from an asset package. The page # resolve pre-warms the referenced # packages, so press-time runs are # cache hits. immediate_client_effects=[ clfx.ScreenMessageV2( message=( strs.effects.immediate_effects_hello ).spec, color=(0, 1, 0), ), clfx.PlaySoundV2( sound=( builtinassets.audio ).cash_register, ), clfx.Delay(1.0), clfx.ScreenMessageV2( message=( strs.effects.effect_success ).spec, color=(0, 1, 0), ), clfx.PlaySoundV2( sound=( builtinassets.audio ).cash_register, ), ] ), ), dui2.Button( label=strs.effects.response_client_effects.spec, size=(120, 80), action=dui2.Browse( dui2.Request('/', args={'test_effects': True}) ), ), dui2.Button( label=strs.effects.immediate_local_action.spec, size=(120, 80), action=dui2.Local( immediate_local_action='testaction', immediate_local_action_args={'testparam': 123}, ), ), dui2.Button( label=strs.effects.response_local_action.spec, size=(120, 80), action=dui2.Browse( dui2.Request('/', args={'test_action': True}) ), ), ], ), dui2.ButtonRow( title=strs.nav.few_more_tests.spec, buttons=[ dui2.Button( label=( strs.common.hide_debug.spec if debug else strs.common.show_debug.spec ), size=(120, 80), action=dui2.Replace( dui2.Request('/', args={'debug': not debug}) ), ), dui2.Button( label=strs.nav.slow_browse.spec, size=(120, 80), action=dui2.Browse(dui2.Request('/slow')), ), dui2.Button( label=strs.nav.slow_replace.spec, size=(120, 80), action=dui2.Replace(dui2.Request('/slow')), ), dui2.Button( label=strs.common.timed_actions.spec, size=(120, 80), action=dui2.Browse(dui2.Request('/timedactions')), ), dui2.Button( label=strs.web.web_get.spec, size=(120, 80), action=dui2.Browse(dui2.Request('/webtest/get')), ), dui2.Button( label=strs.web.web_post.spec, size=(120, 80), action=dui2.Browse( dui2.Request( '/webtest/post', method=dui2.RequestMethod.POST, ) ), ), dui2.Button( label=strs.items.display_items.spec, size=(120, 80), action=dui2.Browse(dui2.Request('/displayitems')), ), dui2.Button( label=strs.layout.empty_page.spec, size=(120, 80), action=dui2.Browse(dui2.Request('/emptypage')), ), ], ), dui2.ButtonRow( title=strs.nav.even_more_tests.spec, buttons=[ dui2.Button( label=strs.cloud.cloud_msg_get.spec, size=(120, 80), action=dui2.Browse( dui2.Request('/cloudmsgtest/get') ), ), dui2.Button( label=strs.cloud.cloud_msg_post.spec, size=(120, 80), action=dui2.Browse( dui2.Request( '/cloudmsgtest/post', method=dui2.RequestMethod.POST, ) ), ), dui2.Button( label=strs.layout.bounds_tests.spec, size=(120, 80), action=dui2.Browse(dui2.Request('/boundstests')), ), ], ), dui2.ButtonRow(title=strs.layout.empty_row.spec, buttons=[]), dui2.ButtonRow( title=strs.layout.layout_tests.spec, debug=debug, padding_left=5.0, buttons=[ dui2.Button( label=strs.nav.test.spec, size=(180, 200), decorations=_layout_test_decos(debug), ), dui2.Button( label=strs.nav.test_two.spec, size=(100, 100), color=(1, 0, 0, 1), label_color=(1, 1, 1, 1), padding_right=4, ), # Should look like the first button but # scaled down. dui2.Button( label=strs.nav.test.spec, size=(180, 200), scale=0.6, padding_bottom=30, # Should nudge us up. debug=debug, # Show bounds. decorations=_layout_test_decos(debug), ), # Testing custom button images and opacity. dui2.Button( label=strs.nav.test_three.spec, texture=builtinassets.textures.button_square_wide, padding_left=10.0, padding_right=10.0, color=(1, 1, 1, 0.3), size=(200, 100), ), # Testing image drawing vs bounds dui2.Button( label=strs.layout.bounds_test.spec, texture=builtinassets.textures.white, color=(1, 1, 1, 0.3), size=(150, 100), debug=debug, ), ], ), dui2.ButtonRow( title=strs.layout.long_row_test.spec, subtitle=strs.layout.look_a_subtitle.spec, buttons=[ dui2.Button( size=(150, 100), decorations=[ dui2.Text( text=strs.layout.max_width_test.spec, position=(0, 25), size=(150 * 0.8, 32.0), flatness=1.0, shadow=0.0, debug=debug, ), # v1 bakes a newline into this one # ('MaxHeightTest\nSecondLine'); we ask # for two balanced lines instead. dui2.Text( text=strs.layout.max_height_test.spec, position=(0, -20), size=(150 * 0.8, 40), flatness=1.0, shadow=0.0, debug=debug, ), ], ), dui2.Button( size=(150, 100), decorations=[ dui2.Image( texture=classicassets.textures.zoe_icon, position=(0, 0), size=(70, 70), tint_texture=( classicassets.textures ).zoe_icon_color_mask, tint_color=(1, 0, 0), tint2_color=(0, 1, 0), mask_texture=( builtinassets.textures ).character_icon_mask, ), ], ), dui2.Button( size=(150, 100), decorations=[ dui2.Image( texture=( classicassets.textures ).bridgit_preview, position=(0, 10), size=(120, 60), mask_texture=( classicassets.textures.map_preview_mask ), mesh_opaque=( classicassets.meshes ).level_select_button_opaque, mesh_transparent=( classicassets.meshes ).level_select_button_transparent, ), ], ), dui2.Button(size=(150, 100)), dui2.Button(size=(150, 100)), dui2.Button(size=(150, 100)), dui2.Button(size=(150, 100)), dui2.Button(size=(150, 100)), dui2.Button(size=(150, 100)), dui2.Button(size=(150, 100)), dui2.Button(size=(150, 100)), dui2.Button( label=strs.common.foo.spec, size=(150, 100), scale=0.4, padding_left=100, padding_right=200, ), dui2.Button(size=(150, 100)), dui2.Button(size=(150, 100)), dui2.Button(size=(150, 100)), dui2.Button(size=(150, 100)), dui2.Button(size=(150, 100)), dui2.Button(size=(150, 100)), dui2.Button(size=(150, 100)), dui2.Button(size=(150, 100)), ], ), dui2.ButtonRow( spacing_top=-15, subtitle=strs.layout.subtitle_only.spec, buttons=[ dui2.Button(size=(200, 120)), ], ), dui2.ButtonRow( buttons=[ dui2.Button( label=strs.layout.row_with_no_title.spec, size=(300, 80), style=dui2.ButtonStyle.MEDIUM, color=(0.8, 0.8, 0.8, 1), icon=classicassets.textures.button_punch, icon_color=(0.5, 0.3, 1.0, 1.0), icon_scale=1.2, ), ], ), dui2.ButtonRow( title=strs.layout.centered_faded_title.spec, title_color=(0.6, 0.6, 1.0, 0.3), title_flatness=1.0, title_shadow=1.0, subtitle=strs.layout.testing_centered.spec, subtitle_color=(1.0, 0.5, 1.0, 0.5), subtitle_flatness=1.0, subtitle_shadow=0.0, center_content=True, center_title=True, buttons=[ dui2.Button( label=strs.common.hello_there.spec, size=(200, 120), color=(0.7, 0.7, 0.9, 1), ), ], ), ], ) ) # Include some client effects if they ask (the 'Response # ClientEffects' button). V2 effect forms; see the immediate-effects # note above. if request.args.get('test_effects', False): response.client_effects = [ clfx.ScreenMessageV2( message=strs.effects.response_effects_hello.spec, color=(0, 1, 0), ), clfx.PlaySoundV2(sound=builtinassets.audio.cash_register), clfx.Delay(1.0), clfx.ScreenMessageV2( message=strs.effects.effect_success.spec, color=(0, 1, 0) ), clfx.PlaySoundV2(sound=builtinassets.audio.cash_register), ] # Include a local-action if they ask (the 'Response LocalAction' # button). if request.args.get('test_action', False): response.local_action = 'testaction' response.local_action_args = {'testparam': 234} return response def _layout_test_decos(debug: bool) -> list[bacommon.docui.v2.Decoration]: """Decorations for the layout-test buttons (used on two buttons).""" import bacommon.docui.v2 as dui2 from bauiv1 import _docuiv2testassets strs = _docuiv2testassets.strings return [ dui2.Image( texture=classicassets.textures.powerup_punch, position=(-70, 0), size=(40, 40), h_align=dui2.HAlign.LEFT, ), dui2.Image( texture=classicassets.textures.powerup_speed, position=(0, 75), size=(35, 35), v_align=dui2.VAlign.TOP, ), dui2.Text( text=strs.layout.corner_tl.spec, position=(-70, 75), size=(50, 50), h_align=dui2.HAlign.LEFT, v_align=dui2.VAlign.TOP, debug=debug, ), dui2.Text( text=strs.layout.corner_tr.spec, position=(70, 75), size=(50, 50), h_align=dui2.HAlign.RIGHT, v_align=dui2.VAlign.TOP, debug=debug, ), dui2.Text( text=strs.layout.corner_bl.spec, position=(-70, -75), size=(50, 50), h_align=dui2.HAlign.LEFT, v_align=dui2.VAlign.BOTTOM, debug=debug, ), dui2.Text( text=strs.layout.corner_br.spec, position=(70, -75), size=(50, 50), h_align=dui2.HAlign.RIGHT, v_align=dui2.VAlign.BOTTOM, debug=debug, ), ] def _test_v2_page_2( request: bacommon.docui.v2.Request, ) -> bacommon.docui.v2.Response: """More testing (v2 mirror of the v1 '/test2' page).""" import bacommon.docui.v2 as dui2 from bauiv1 import _docuiv2testassets del request # Unused. strs = _docuiv2testassets.strings return dui2.Response( page=dui2.Page( title=strs.nav.test_two_title.spec, rows=[ dui2.ButtonRow( title=strs.nav.more_tests.spec, buttons=[ dui2.Button( label=strs.nav.browse.spec, size=(120, 80), action=dui2.Browse(dui2.Request('/')), ), dui2.Button( label=strs.nav.replace.spec, size=(120, 80), action=dui2.Replace(dui2.Request('/')), ), dui2.Button( label=strs.nav.close.spec, size=(120, 80), action=dui2.Local(close_window=True), selected=True, # Testing this ), ], ), ], ) ) def _test_v2_page_long( request: bacommon.docui.v2.Request, ) -> bacommon.docui.v2.Response: """Testing a page that takes a bit of time to load (v2 mirror).""" import bacommon.docui.v2 as dui2 from bauiv1 import _docuiv2testassets del request # Unused. strs = _docuiv2testassets.strings # Simulate a slow connection or whatnot. time.sleep(3.0) return dui2.Response( page=dui2.Page( title=strs.nav.test.spec, center_vertically=True, rows=[ dui2.ButtonRow( title=strs.common.that_took_a_while.spec, center_title=True, center_content=True, buttons=[ dui2.Button( label=strs.common.sure_did.spec, size=(120, 80), action=dui2.Browse(dui2.Request('/')), ), ], ), ], ) ) def _test_v2_page_timed_actions( request: bacommon.docui.v2.Request, ) -> bacommon.docui.v2.Response: """Testing timed actions (v2 mirror of '/timedactions').""" import bacommon.docui.v2 as dui2 from bauiv1 import _docuiv2testassets strs = _docuiv2testassets.strings val = request.args.get('val') if not isinstance(val, int): val = 5 return dui2.Response( page=dui2.Page( title=strs.nav.test.spec, center_vertically=True, rows=[ dui2.ButtonRow( title=strs.common.hello_there_num(num=str(val)).spec, subtitle=strs.common.each_change.spec, center_title=True, center_content=True, buttons=[ dui2.Button( label=strs.nav.done.spec, size=(120, 80), action=dui2.Local(close_window=True), default=True, ), ], ), ], ), # Refresh this page with a countdown until we hit zero and then # close the window. timed_action=( dui2.Replace(dui2.Request('/timedactions', args={'val': val - 1})) if (val - 1) > 0 else dui2.Local(close_window=True) ), timed_action_delay=1.0, ) def _test_v2_page_empty( request: bacommon.docui.v2.Request, ) -> bacommon.docui.v2.Response: """An empty page (v2 mirror of '/emptypage').""" import bacommon.docui.v2 as dui2 from bauiv1 import _docuiv2testassets del request # Unused. return dui2.Response( page=dui2.Page( title=_docuiv2testassets.strings.layout.empty_page_title.spec, rows=[], ) ) def _test_v2_page_display_items( request: bacommon.docui.v2.Request, ) -> bacommon.docui.v2.Response: """Testing display-items (v2 mirror of '/displayitems').""" from bacommon.classic import ClassicChestAppearance, ClassicChestDisplayItem import bacommon.displayitem as ditm import bacommon.docui.v2 as dui2 from bauiv1 import _docuiv2testassets strs = _docuiv2testassets.strings # Show some specific debug bits if they ask us to. debug = bool(request.args.get('debug', False)) def _make_test_button( scale: float, wrapper: ditm.Wrapper, ) -> bacommon.docui.v2.Button: # See how this looks when unrecognized (relying on wrapper info # only). uwrapper = copy.deepcopy(wrapper) uwrapper.item = ditm.Unknown() return dui2.Button( size=(300, 400), scale=scale, decorations=[ dui2.DisplayItem( wrapper=wrapper, style=dui2.DisplayItemStyle.FULL, position=(-62, 100), size=(120, 120), debug=debug, ), dui2.DisplayItem( wrapper=uwrapper, style=dui2.DisplayItemStyle.FULL, position=(62, 100), size=(120, 120), debug=debug, ), dui2.DisplayItem( wrapper=wrapper, style=dui2.DisplayItemStyle.COMPACT, position=(-55, -20), size=(80, 80), debug=debug, ), dui2.DisplayItem( wrapper=uwrapper, style=dui2.DisplayItemStyle.COMPACT, position=(55, -20), size=(80, 80), debug=debug, ), dui2.DisplayItem( wrapper=wrapper, style=dui2.DisplayItemStyle.ICON, position=(-55, -120), size=(100, 80), debug=debug, ), dui2.DisplayItem( wrapper=uwrapper, style=dui2.DisplayItemStyle.ICON, position=(55, -120), size=(100, 80), debug=debug, ), ], ) return dui2.Response( page=dui2.Page( padding_left=20, padding_right=20, title=strs.items.display_items.spec, rows=[ dui2.ButtonRow( debug=debug, padding_left=-10, title=strs.items.display_item_tests.spec, subtitle=strs.items.display_items_sub.spec, buttons=[ _make_test_button( 1.0, ditm.Wrapper.for_item(ditm.Tickets(count=213)), ), _make_test_button( 0.47, ditm.Wrapper.for_item(ditm.Tickets(count=213)), ), _make_test_button( 1.0, ditm.Wrapper.for_item( ClassicChestDisplayItem( appearance=ClassicChestAppearance.L3 ) ), ), _make_test_button( 1.0, ditm.Wrapper.for_item(ditm.Tokens(count=3)), ), _make_test_button( 1.0, ditm.Wrapper.for_item(ditm.Tokens(count=1414287)), ), _make_test_button( 1.0, ditm.Wrapper.for_item(ditm.Test()), ), ], ), dui2.ButtonRow( buttons=[ dui2.Button( label=( strs.common.hide_debug.spec if debug else strs.common.show_debug.spec ), style=dui2.ButtonStyle.MEDIUM, size=(240, 60), color=(0.6, 0.4, 0.8, 1.0), action=dui2.Replace( dui2.Request( request.path, args={'debug': not debug} ) ), ) ], ), ], ) ) def _test_v2_bounds( request: bacommon.docui.v2.Request, ) -> bacommon.docui.v2.Response: """Button-style bounds tests (v2 mirror of '/boundstests').""" import bacommon.docui.v2 as dui2 from bauiv1 import _docuiv2testassets del request # Unused. strs = _docuiv2testassets.strings def _nm(style: bacommon.docui.v2.ButtonStyle) -> LangStrSpec: # Code-literal pass-through: renders the raw enum name # verbatim in every locale. return strs.common.code_literal( text=f'{type(style).__name__}.{style.name}' ).spec def _hello_row( title: LangStrSpec, sizes: list[tuple[float, float]], style: bacommon.docui.v2.ButtonStyle, texture: bool = False, ) -> bacommon.docui.v2.ButtonRow: return dui2.ButtonRow( title=title, buttons=[ dui2.Button( label=strs.layout.hello.spec, size=size, style=style, texture=(builtinassets.textures.white if texture else None), color=(1, 0, 0, 0.3) if texture else None, debug=True, ) for size in sizes ], ) styles = dui2.ButtonStyle return dui2.Response( page=dui2.Page( title=strs.layout.bounds_tests_title.spec, rows=[ _hello_row( _nm(styles.SQUARE), [(300, 300), (200, 200), (100, 100)], styles.SQUARE, ), _hello_row( _nm(styles.SQUARE_WIDE), [(400, 200), (200, 250), (60, 100)], styles.SQUARE_WIDE, ), _hello_row( strs.layout.background_texture.spec, [(300, 300), (200, 200), (100, 100)], styles.SQUARE, texture=True, ), _hello_row( _nm(styles.TAB), [(400, 100), (200, 50), (100, 60)], styles.TAB, ), _hello_row( _nm(styles.LARGER), [(500, 100), (200, 50), (100, 60)], styles.LARGER, ), _hello_row( _nm(styles.LARGE), [(400, 100), (200, 50), (100, 60)], styles.LARGE, ), _hello_row( _nm(styles.MEDIUM), [(300, 100), (200, 50), (100, 60)], styles.MEDIUM, ), _hello_row( _nm(styles.SMALL), [(200, 100), (200, 50), (100, 60)], styles.SMALL, ), _hello_row( _nm(styles.BACK), [(200, 100), (200, 50), (100, 60)], styles.BACK, ), _hello_row( _nm(styles.BACK_SMALL), [(200, 100), (200, 50), (100, 60)], styles.BACK_SMALL, ), ], ) ) # 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