Source code for bauiv1lib.docuitestdepictions

# Released under the MIT License. See LICENSE for details.
#
"""The doc-ui depictions test page.

Shows each depiction kind as a
:class:`bacommon.docui.v2.Depiction` decoration on a button, then on a
disabled button, then fitted into boxes of other shapes and alignments.
Hovering, pressing, and selecting the buttons exercises how depictions
follow their button's emphasis; the disabled row, the standard disabled
look. The precise-fit rows check the fit exactly: a flat white
depiction of a known shape, with a half-black image laid over where
it should land (computed here, independently of the native fit), so
a correct fit is a plain grey rect and any offset shows as a white or
dark fringe. The button-body rows do the same with the white shape as
the button's own body (its depiction).

Characters come from the classic character package's wrapper (built
from its .bchar files), the way game code gets them; names are plain
text.
"""

from __future__ import annotations  # Docs-generation hack.

from typing import TYPE_CHECKING, assert_never
from dataclasses import replace

from bacommon.langstr import LangStrSpecValue
import bacommon.clienteffect as clfx
import bacommon.depiction as bdep
import bacommon.docui.v2 as dui2

if TYPE_CHECKING:
    import bacommon.assetspec
    import bacommon.docui.v2
    import bacommon.docui.routes.docuitest
    from bauiv1 import _classiccharacterassets


def _characters() -> _classiccharacterassets.CharactersGroup:
    """The classic characters, as the ui wrapper gives them."""
    from bauiv1 import _classiccharacterassets

    return _classiccharacterassets.characters


def _zoe_viewer() -> bdep.CharacterViewerDepiction:
    """A live viewer of Zoe (only the scene wrapper carries spazzes)."""
    from bascenev1 import _classiccharacterassets

    return _classiccharacterassets.characters.zoe.get_viewer_depiction()


def _name(text: str) -> bdep.NameDepiction:
    """A name depiction showing some plain text."""
    from bascenev1 import plain_name_json

    return bdep.NameDepiction(plain_name_json(text))


#: Each test button's size, and where its depiction box centers.
_BUTTON_SIZE = (170.0, 190.0)
_BOX_CENTER = (0.0, -12.0)

#: The box most kinds get: square, filling most of the button.
_BOX = (130.0, 130.0)

#: Precise-fit cases: (button size, depiction aspect, h align, v
#: align). Each depiction box fills its button but for a margin and a
#: caption strip.
_PRECISE_SQUARE: list[
    tuple[tuple[float, float], float, dui2.HAlign, dui2.VAlign]
] = [
    ((150.0, 160.0), 2.0, dui2.HAlign.CENTER, dui2.VAlign.TOP),
    ((150.0, 160.0), 2.0, dui2.HAlign.CENTER, dui2.VAlign.CENTER),
    ((150.0, 160.0), 2.0, dui2.HAlign.CENTER, dui2.VAlign.BOTTOM),
    ((150.0, 160.0), 0.5, dui2.HAlign.LEFT, dui2.VAlign.CENTER),
    ((150.0, 160.0), 0.5, dui2.HAlign.CENTER, dui2.VAlign.CENTER),
    ((150.0, 160.0), 0.5, dui2.HAlign.RIGHT, dui2.VAlign.CENTER),
    ((150.0, 160.0), 1.0, dui2.HAlign.CENTER, dui2.VAlign.CENTER),
]
_PRECISE_SHAPED: list[
    tuple[tuple[float, float], float, dui2.HAlign, dui2.VAlign]
] = [
    ((240.0, 130.0), 1.0, dui2.HAlign.LEFT, dui2.VAlign.CENTER),
    ((240.0, 130.0), 1.0, dui2.HAlign.CENTER, dui2.VAlign.CENTER),
    ((240.0, 130.0), 1.0, dui2.HAlign.RIGHT, dui2.VAlign.CENTER),
    ((240.0, 130.0), 3.5, dui2.HAlign.LEFT, dui2.VAlign.BOTTOM),
    ((120.0, 240.0), 1.0, dui2.HAlign.CENTER, dui2.VAlign.TOP),
    ((120.0, 240.0), 1.0, dui2.HAlign.CENTER, dui2.VAlign.CENTER),
    ((120.0, 240.0), 1.0, dui2.HAlign.CENTER, dui2.VAlign.BOTTOM),
    ((120.0, 240.0), 0.3, dui2.HAlign.RIGHT, dui2.VAlign.TOP),
]

#: A precise-fit case for a button body: (button size, button scale,
#: depiction aspect, h align, v align, button style). The depiction is
#: the button itself, so its box is the button's whole box.
type _BodyCase = tuple[
    tuple[float, float],
    float,
    float,
    dui2.HAlign,
    dui2.VAlign,
    dui2.ButtonStyle,
]

_PRECISE_BODIES: list[_BodyCase] = [
    (
        (150.0, 150.0),
        1.0,
        2.0,
        dui2.HAlign.CENTER,
        dui2.VAlign.TOP,
        dui2.ButtonStyle.SQUARE,
    ),
    (
        (150.0, 150.0),
        1.0,
        2.0,
        dui2.HAlign.CENTER,
        dui2.VAlign.CENTER,
        dui2.ButtonStyle.SQUARE,
    ),
    (
        (150.0, 150.0),
        1.0,
        2.0,
        dui2.HAlign.CENTER,
        dui2.VAlign.BOTTOM,
        dui2.ButtonStyle.SQUARE,
    ),
    (
        (150.0, 150.0),
        1.0,
        0.5,
        dui2.HAlign.LEFT,
        dui2.VAlign.CENTER,
        dui2.ButtonStyle.SQUARE,
    ),
    (
        (150.0, 150.0),
        1.0,
        0.5,
        dui2.HAlign.RIGHT,
        dui2.VAlign.CENTER,
        dui2.ButtonStyle.SQUARE,
    ),
    (
        (150.0, 150.0),
        1.0,
        1.0,
        dui2.HAlign.CENTER,
        dui2.VAlign.CENTER,
        dui2.ButtonStyle.SQUARE,
    ),
]
_PRECISE_BODIES_SCALED: list[_BodyCase] = [
    (
        (240.0, 120.0),
        0.8,
        1.0,
        dui2.HAlign.LEFT,
        dui2.VAlign.CENTER,
        dui2.ButtonStyle.SQUARE,
    ),
    (
        (240.0, 120.0),
        0.8,
        1.0,
        dui2.HAlign.RIGHT,
        dui2.VAlign.CENTER,
        dui2.ButtonStyle.SQUARE,
    ),
    (
        (120.0, 180.0),
        1.3,
        1.0,
        dui2.HAlign.CENTER,
        dui2.VAlign.TOP,
        dui2.ButtonStyle.SQUARE,
    ),
    (
        (120.0, 180.0),
        1.3,
        1.0,
        dui2.HAlign.CENTER,
        dui2.VAlign.BOTTOM,
        dui2.ButtonStyle.SQUARE,
    ),
    (
        (200.0, 100.0),
        1.0,
        2.0,
        dui2.HAlign.CENTER,
        dui2.VAlign.CENTER,
        dui2.ButtonStyle.MEDIUM,
    ),
    (
        (200.0, 100.0),
        1.0,
        3.0,
        dui2.HAlign.LEFT,
        dui2.VAlign.BOTTOM,
        dui2.ButtonStyle.LARGE,
    ),
]

#: Room above a button-body case for its caption.
_PRECISE_BODY_CAPTION = 26.0

#: Precise-fit layout: margin around the box, and the caption strip
#: above it.
_PRECISE_MARGIN = 12.0
_PRECISE_CAPTION = 24.0


def _expected_fit(
    box: tuple[float, float],
    aspect: float,
    h_align: dui2.HAlign,
    v_align: dui2.VAlign,
) -> tuple[tuple[float, float], tuple[float, float]]:
    """Where a shape should land in a box: (center offset, size).

    Written out here rather than shared with the native fit, so the
    page checks one against the other.
    """
    bwidth, bheight = box
    if bwidth / bheight > aspect:
        width, height = bheight * aspect, bheight
    else:
        width, height = bwidth, bwidth / aspect
    xslack = (bwidth - width) * 0.5
    yslack = (bheight - height) * 0.5
    if h_align is dui2.HAlign.LEFT:
        xoffs = -xslack
    elif h_align is dui2.HAlign.CENTER:
        xoffs = 0.0
    elif h_align is dui2.HAlign.RIGHT:
        xoffs = xslack
    else:
        assert_never(h_align)
    if v_align is dui2.VAlign.TOP:
        yoffs = yslack
    elif v_align is dui2.VAlign.CENTER:
        yoffs = 0.0
    elif v_align is dui2.VAlign.BOTTOM:
        yoffs = -yslack
    else:
        assert_never(v_align)
    return (xoffs, yoffs), (width, height)


def _indexed_image(
    texture: bacommon.assetspec.TextureSpec,
) -> bdep.ImageDepiction:
    """An image depiction in the compact (indexed) wire form.

    Built here against the client's own listing, as a producer would
    against its vendored one, so the page exercises the indexed decode
    path (manifest + digest check + index resolve).
    """
    from bacommon.assetspec import AssetIndexContext, wire_digest
    from bauiv1lib.docui._resolve import package_asset_listing

    packages = [texture._apvernum]  # pylint: disable=protected-access
    ctx = AssetIndexContext(packages, package_asset_listing)
    return bdep.ImageDepiction(
        texture=ctx.to_index(texture),
        packages=packages,
        domain_digest=wire_digest(ctx.domain_digest()),
    )


[docs] def test_page_depictions( route: bacommon.docui.routes.docuitest.Depictions, ) -> bacommon.docui.v2.Response: """Testing depictions.""" from bauiv1 import ( _classiccatalogassets, _commonassets, _docuiv2testassets, ) strs = _docuiv2testassets.strings debug = route.debug tex = _classiccatalogassets.textures chest = bdep.ImageDepiction( texture=tex.chest_icon, tint_texture=tex.chest_icon_tint, tint_color=(0.8, 0.2, 1.0), tint2_color=(1.0, 0.8, 0.2), ) kinds: list[tuple[str, bdep.Depiction, tuple[float, float]]] = [ ('icon', _characters().spaz.get_icon_depiction(), _BOX), ('icon (zoe)', _characters().zoe.get_icon_depiction(), _BOX), ('name', _name('Zoe'), (140.0, 50.0)), ('image (chest)', chest, _BOX), ('image (coin)', bdep.ImageDepiction(texture=tex.coin), _BOX), ( 'image (scale 1.5)', bdep.ImageDepiction(texture=tex.coin, scale=1.5), _BOX, ), ( 'image (scale 0.5)', bdep.ImageDepiction(texture=tex.coin, scale=0.5), _BOX, ), ('image (indexed)', _indexed_image(tex.coin), _BOX), ( 'viewer', _zoe_viewer(), _BOX, ), ('unknown kind', bdep.UnknownDepiction(), _BOX), ] def _button( label: str, decorations: list[dui2.Decoration], *, disabled: bool = False, ) -> dui2.Button: """A test button: a caption over the given decorations.""" return dui2.Button( size=_BUTTON_SIZE, disabled=disabled, decorations=[ dui2.Text( text=LangStrSpecValue.literal(label), position=(0.0, 75.0), size=(160.0, 0.0), scale=0.55, ) ] + decorations, ) def _depiction( depiction: bdep.Depiction, size: tuple[float, float], *, position: tuple[float, float] = _BOX_CENTER, h_align: dui2.HAlign = dui2.HAlign.CENTER, v_align: dui2.VAlign = dui2.VAlign.CENTER, ) -> dui2.Depiction: return dui2.Depiction( depiction=depiction, position=position, size=size, h_align=h_align, v_align=v_align, debug=debug, ) def _kind_buttons(*, disabled: bool) -> list[dui2.Button]: return [ _button(label, [_depiction(dep, size)], disabled=disabled) for label, dep, size in kinds ] wide = (150.0, 70.0) tall = (70.0, 140.0) zoe = _characters().zoe.get_icon_depiction() fit_buttons = [ _button( 'wide, left', [_depiction(zoe, wide, h_align=dui2.HAlign.LEFT)], ), _button('wide, center', [_depiction(zoe, wide)]), _button( 'wide, right', [_depiction(zoe, wide, h_align=dui2.HAlign.RIGHT)], ), _button( 'tall, top', [_depiction(zoe, tall, v_align=dui2.VAlign.TOP)], ), _button( 'tall, bottom', [_depiction(zoe, tall, v_align=dui2.VAlign.BOTTOM)], ), _button( 'long name', [_depiction(_name('A Rather Long Name'), (140.0, 50.0))], ), _button( 'icon over name', [ _depiction( _characters().kronk.get_icon_depiction(), (140.0, 90.0), position=(0.0, 5.0), ), _depiction( _name('A Rather Long Name'), (140.0, 32.0), position=(0.0, -65.0), ), ], ), ] white = _commonassets.textures.white def _precise_button( size: tuple[float, float], aspect: float, h_align: dui2.HAlign, v_align: dui2.VAlign, ) -> dui2.Button: """A white shape fitted into a box, the expected fit over it.""" box = ( size[0] - 2.0 * _PRECISE_MARGIN, size[1] - 2.0 * _PRECISE_MARGIN - _PRECISE_CAPTION, ) boxcenter = (0.0, -0.5 * _PRECISE_CAPTION) (xoffs, yoffs), expected = _expected_fit(box, aspect, h_align, v_align) caption = ( f'{aspect:g}:1, {h_align.name.lower()}, {v_align.name.lower()}' ) return dui2.Button( size=size, style=( dui2.ButtonStyle.MEDIUM if size[0] > size[1] else dui2.ButtonStyle.SQUARE ), decorations=[ dui2.Text( text=LangStrSpecValue.literal(caption), position=(0.0, 0.5 * size[1] - 0.5 * _PRECISE_CAPTION), size=(size[0] - 10.0, 0.0), scale=0.5, ), dui2.Depiction( depiction=bdep.ImageDepiction(texture=white, aspect=aspect), position=boxcenter, size=box, h_align=h_align, v_align=v_align, highlight=False, debug=debug, ), dui2.Image( texture=white, position=(boxcenter[0] + xoffs, boxcenter[1] + yoffs), size=expected, color=(0.0, 0.0, 0.0, 0.5), highlight=False, ), ], ) def _precise_body_button(case: _BodyCase) -> dui2.Button: """A button whose body is a white shape, the expected fit over it. Decorations sit in the button's own (unscaled) space, centered on it, so the expected fit is in the button's box as given. """ size, scale, aspect, h_align, v_align, style = case (xoffs, yoffs), expected = _expected_fit(size, aspect, h_align, v_align) caption = ( f'{aspect:g}:1, {h_align.name.lower()}, {v_align.name.lower()}' f', x{scale:g}, {style.name.lower()}' ) return dui2.Button( size=size, scale=scale, style=style, padding_top=_PRECISE_BODY_CAPTION / scale, depiction=bdep.ImageDepiction(texture=white, aspect=aspect), depiction_h_align=h_align, depiction_v_align=v_align, debug=debug, decorations=[ dui2.Text( text=LangStrSpecValue.literal(caption), position=( 0.0, 0.5 * size[1] + 0.5 * _PRECISE_BODY_CAPTION / scale, ), size=(size[0], 0.0), scale=0.45 / scale, highlight=False, ), dui2.Image( texture=white, position=(xoffs, yoffs), size=expected, color=(0.0, 0.0, 0.0, 0.5), highlight=False, ), ], ) def _hit_button( caption: str, size: tuple[float, float], h_align: dui2.HAlign, *, hit_area: bool, ) -> dui2.Button: """A wide button whose body is an icon at one end. Pressing says which button got it, so it's easy to see where each one takes presses. """ return dui2.Button( size=size, style=dui2.ButtonStyle.MEDIUM, padding_top=_PRECISE_BODY_CAPTION, depiction=_characters().zoe.get_icon_depiction(), depiction_h_align=h_align, depiction_hit_area=hit_area, debug=debug, action=dui2.Local( immediate_client_effects=[ # Plain-string form: a local page has no decode # context for ScreenMessageV2's l-strings. clfx.ScreenMessage( message=f'Pressed: {caption}', color=(0.5, 1.0, 0.5), ) ] ), decorations=[ dui2.Text( text=LangStrSpecValue.literal(caption), position=(0.0, 0.5 * size[1] + 0.5 * _PRECISE_BODY_CAPTION), size=(size[0], 0.0), scale=0.5, highlight=False, ), ], ) hit_buttons = [ _hit_button( 'whole button', (320.0, 110.0), dui2.HAlign.LEFT, hit_area=False ), _hit_button( 'icon only', (320.0, 110.0), dui2.HAlign.LEFT, hit_area=True ), _hit_button( 'icon only (right)', (320.0, 110.0), dui2.HAlign.RIGHT, hit_area=True, ), _hit_button( 'thin: icon grown to min size', (320.0, 40.0), dui2.HAlign.LEFT, hit_area=True, ), ] def _row( title: str, subtitle: str, buttons: list[dui2.Button] ) -> dui2.ButtonRow: return dui2.ButtonRow( debug=debug, title=LangStrSpecValue.literal(title), subtitle=LangStrSpecValue.literal(subtitle), buttons=buttons, ) return dui2.Response( page=dui2.Page( title=LangStrSpecValue.literal('Depictions'), rows=[ _row( 'Kinds', 'Each kind on a button; hover, press, and select them' ' to see them follow the button.', _kind_buttons(disabled=False), ), _row( 'Disabled', 'The same on disabled buttons: faded and greyed.', _kind_buttons(disabled=True), ), _row( 'Fit and alignment', 'A square icon fitted into other shapes, a name' ' shrinking to fit, and a composite.', fit_buttons, ), _row( 'Hit area', 'Icon bodies on wide buttons. The first takes presses' ' and hover anywhere in its box; the rest only over' ' the icon (grown to a minimum size).', hit_buttons, ), _row( 'Precise fit', 'White shapes fitted into boxes, the expected fit' ' half-black over each: any fringe is a misfit.', [_precise_button(*case) for case in _PRECISE_SQUARE], ), _row( 'Precise fit (shaped boxes)', 'The same in wide and tall boxes.', [_precise_button(*case) for case in _PRECISE_SHAPED], ), _row( 'Precise fit (button bodies)', 'Each white shape is the button itself (its depiction' ' body), the expected fit half-black over it.', [_precise_body_button(case) for case in _PRECISE_BODIES], ), _row( 'Precise fit (button bodies, scaled and styled)', 'The same on scaled buttons and other styles.', [ _precise_body_button(case) for case in _PRECISE_BODIES_SCALED ], ), 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=replace(route, debug=not debug).replace(), ) ], ), ], ) )
# 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, TypeIs) import asyncio from concurrent.futures import Future from pathlib import Path from enum import Enum