# Released under the MIT License. See LICENSE for details.
#
"""Version 2 doc-ui types -- language-agnostic (l-string) text.
Where v1 carries pre-localized raw ``str`` text (optionally a JSON-encoded
legacy ``babase.Lstr`` via ``*_is_lstr`` flags) and expects the *server* to
localize, v2 text is always a language-agnostic
:class:`~bacommon.langstr.LangStrSpec`. The server ships one response to every
client regardless of language; the client resolves the referenced
asset-packages in its own locale and decodes the strings at render time.
See ``docs/initiatives/docui-v2-lstrings.md`` (ballistica-internal). This is
the milestone-1 slice: a minimal but real subset of the v1 element set, with
text typed as ``LangStrSpec`` (the name-based form -- subs are flat for now).
Non-text fields mirror v1's names/keys so client render code can stay close
to ``v1prep``.
"""
from __future__ import annotations # Docs-generation hack.
from enum import Enum
from dataclasses import dataclass, field
from typing import Annotated, override, assert_never
from efro.dataclassio import ioprepped, IOAttrs, IOMultiType
import bacommon.clienteffect as clfx
import bacommon.displayitem as ditm
from bacommon.langstr import LangStrSpec
from bacommon.assetspec import TextureSpec, MeshSpec
from bacommon.docui._docui import (
DocUIRequest,
DocUIRequestTypeID,
DocUIResponse,
DocUIResponseTypeID,
)
[docs]
class RequestMethod(Enum):
"""Type of requests that can be made to doc-ui servers."""
#: An unknown request method (newer client -> older server).
UNKNOWN = 'u'
#: Fetch some resource. Retriable; results optionally cacheable.
GET = 'g'
#: Change some resource. Not implicitly retriable, not cacheable.
POST = 'p'
[docs]
@ioprepped
@dataclass
class Request(DocUIRequest):
"""Full request to doc-ui (v2)."""
path: str
method: RequestMethod = RequestMethod.GET
args: dict = field(
default_factory=dict
)
[docs]
@override
@classmethod
def get_type_id(cls) -> DocUIRequestTypeID:
return DocUIRequestTypeID.V2
[docs]
class ActionTypeID(Enum):
"""Type ID for each of our subclasses."""
BROWSE = 'b'
REPLACE = 'r'
LOCAL = 'l'
UNKNOWN = 'u'
[docs]
class Action(IOMultiType[ActionTypeID]):
"""Something that happens when a button is pressed."""
[docs]
@override
@classmethod
def get_type_id(cls) -> ActionTypeID:
raise NotImplementedError()
[docs]
@override
@classmethod
def get_type(cls, type_id: ActionTypeID) -> type[Action]:
# pylint: disable=cyclic-import
t = ActionTypeID
if type_id is t.BROWSE:
return Browse
if type_id is t.REPLACE:
return Replace
if type_id is t.LOCAL:
return Local
if type_id is t.UNKNOWN:
return UnknownAction
assert_never(type_id)
[docs]
@override
@classmethod
def get_type_id_storage_name(cls) -> str:
return '_t'
[docs]
@override
@classmethod
def get_unknown_type_fallback(cls) -> Action:
return UnknownAction()
[docs]
@ioprepped
@dataclass
class UnknownAction(Action):
"""Action type we don't recognize."""
[docs]
@override
@classmethod
def get_type_id(cls) -> ActionTypeID:
return ActionTypeID.UNKNOWN
[docs]
@ioprepped
@dataclass
class Browse(Action):
"""Browse to a new page in a new window."""
request: Request
#: Plays a swish.
default_sound: bool = True
[docs]
@override
@classmethod
def get_type_id(cls) -> ActionTypeID:
return ActionTypeID.BROWSE
[docs]
@ioprepped
@dataclass
class Replace(Action):
"""Replace the current page with a new one (seamless transition)."""
request: Request
#: Plays a click if triggered by a button press.
default_sound: bool = True
[docs]
@override
@classmethod
def get_type_id(cls) -> ActionTypeID:
return ActionTypeID.REPLACE
[docs]
@ioprepped
@dataclass
class Local(Action):
"""Perform only local actions; no new requests or page changes."""
close_window: bool = False
#: Plays a swish if closing the window, else a click.
default_sound: bool = True
#: Client-effects to run immediately when the button is pressed.
#: Note that effect payloads are not yet v2-native — text in them is
#: raw/legacy-lstr, pending clienteffect gaining a resolve-context
#: concept (see the SoundSpec followup in docs/followups.md).
#:
#: :meta private:
immediate_client_effects: list[clfx.Effect] = field(default_factory=list)
#: Local action to run immediately when the button is pressed. Will
#: be handled by
#: :meth:`bauiv1lib.docui.DocUIController.local_action()`.
immediate_local_action: str | None = None
immediate_local_action_args: dict | None = None
[docs]
@override
@classmethod
def get_type_id(cls) -> ActionTypeID:
return ActionTypeID.LOCAL
[docs]
class HAlign(Enum):
"""Horizontal alignment."""
LEFT = 'l'
CENTER = 'c'
RIGHT = 'r'
[docs]
class VAlign(Enum):
"""Vertical alignment."""
TOP = 't'
CENTER = 'c'
BOTTOM = 'b'
[docs]
class DecorationTypeID(Enum):
"""Type ID for each of our subclasses."""
UNKNOWN = 'u'
TEXT = 't'
IMAGE = 'i'
DISPLAY_ITEM = 'd'
[docs]
class Decoration(IOMultiType[DecorationTypeID]):
"""Top level class for our decoration multitype."""
[docs]
@override
@classmethod
def get_type_id(cls) -> DecorationTypeID:
raise NotImplementedError()
[docs]
@override
@classmethod
def get_type(cls, type_id: DecorationTypeID) -> type[Decoration]:
# pylint: disable=cyclic-import
t = DecorationTypeID
if type_id is t.UNKNOWN:
return UnknownDecoration
if type_id is t.TEXT:
return Text
if type_id is t.IMAGE:
return Image
if type_id is t.DISPLAY_ITEM:
return DisplayItem
assert_never(type_id)
[docs]
@override
@classmethod
def get_unknown_type_fallback(cls) -> Decoration:
return UnknownDecoration()
[docs]
@override
@classmethod
def get_type_id_storage_name(cls) -> str:
return '_t'
[docs]
@ioprepped
@dataclass
class UnknownDecoration(Decoration):
"""An unknown decoration (should never reach a client in practice)."""
[docs]
@override
@classmethod
def get_type_id(cls) -> DecorationTypeID:
return DecorationTypeID.UNKNOWN
[docs]
@ioprepped
@dataclass
class Text(Decoration):
"""Text decoration.
``text`` is a language-agnostic :class:`~bacommon.langstr.LangStrSpec`.
"""
text: LangStrSpec
position: tuple[float, float]
#: Effectively max-width and max-height.
size: tuple[float, float]
scale: float = 1.0
h_align: HAlign = (
HAlign.CENTER
)
v_align: VAlign = (
VAlign.CENTER
)
color: tuple[float, float, float, float] | None = None
flatness: float | None = None
shadow: float | None = None
highlight: bool = True
depth_range: tuple[float, float] | None = None
#: Show max-width/height bounds; useful during development.
debug: bool = False
[docs]
@override
@classmethod
def get_type_id(cls) -> DecorationTypeID:
return DecorationTypeID.TEXT
[docs]
@ioprepped
@dataclass
class Image(Decoration):
"""Image decoration. Textures/meshes are language-independent refs.
Unlike text, image assets need no per-locale decode; each ref
(:class:`~bacommon.assetspec.TextureSpec` /
:class:`~bacommon.assetspec.MeshSpec`) is resolved by the client and
rendered directly.
"""
texture: TextureSpec
position: tuple[float, float]
size: tuple[float, float]
color: tuple[float, float, float, float] | None = None
h_align: HAlign = (
HAlign.CENTER
)
v_align: VAlign = (
VAlign.CENTER
)
tint_texture: TextureSpec | None = None
tint_color: tuple[float, float, float] | None = None
tint2_color: tuple[float, float, float] | None = None
mask_texture: TextureSpec | None = None
mesh_opaque: MeshSpec | None = None
mesh_transparent: MeshSpec | None = None
highlight: bool = True
depth_range: tuple[float, float] | None = None
[docs]
@override
@classmethod
def get_type_id(cls) -> DecorationTypeID:
return DecorationTypeID.IMAGE
[docs]
class DisplayItemStyle(Enum):
"""Styles a display-item can be drawn in (mirrors v1)."""
#: Fully conveys what the item is. Draws in a 4x3 box and works
#: best with large-ish displays.
FULL = 'f'
#: Fully conveys the item, condensed into a 2x1 box for small sizes.
COMPACT = 'c'
#: Graphics-only representation in a 1x1 box, for use alongside a
#: textual description.
ICON = 'i'
[docs]
@ioprepped
@dataclass
class DisplayItem(Decoration):
"""DisplayItem decoration.
The wrapped :class:`~bacommon.displayitem.Wrapper` already
localizes its own text client-side, so it carries over from v1
unchanged.
"""
wrapper: ditm.Wrapper
position: tuple[float, float]
size: tuple[float, float]
style: DisplayItemStyle = (
DisplayItemStyle.FULL
)
text_color: tuple[float, float, float] | None = None
highlight: bool = True
depth_range: tuple[float, float] | None = None
debug: bool = False
[docs]
@override
@classmethod
def get_type_id(cls) -> DecorationTypeID:
return DecorationTypeID.DISPLAY_ITEM
[docs]
class RowTypeID(Enum):
"""Type ID for each of our subclasses."""
BUTTON_ROW = 'b'
UNKNOWN = 'u'
[docs]
class Row(IOMultiType[RowTypeID]):
"""Top level class for our row multitype."""
[docs]
@override
@classmethod
def get_type_id(cls) -> RowTypeID:
raise NotImplementedError()
[docs]
@override
@classmethod
def get_type(cls, type_id: RowTypeID) -> type[Row]:
# pylint: disable=cyclic-import
t = RowTypeID
if type_id is t.UNKNOWN:
return UnknownRow
if type_id is t.BUTTON_ROW:
return ButtonRow
assert_never(type_id)
[docs]
@override
@classmethod
def get_unknown_type_fallback(cls) -> Row:
return UnknownRow()
[docs]
@override
@classmethod
def get_type_id_storage_name(cls) -> str:
return '_t'
[docs]
@ioprepped
@dataclass
class UnknownRow(Row):
"""A row type we don't have."""
[docs]
@override
@classmethod
def get_type_id(cls) -> RowTypeID:
return RowTypeID.UNKNOWN
[docs]
@ioprepped
@dataclass
class Page:
"""Doc-UI page version 2.
``title`` is a language-agnostic :class:`~bacommon.langstr.LangStrSpec`.
"""
title: LangStrSpec
rows: list[Row]
#: Center content vertically when it's smaller than the available height.
center_vertically: bool = (
False
)
row_spacing: float = 10.0
#: If things disappear when scrolling up/down, turn this up.
simple_culling_v: float = (
100.0
)
padding_bottom: float = 0.0
padding_left: float = 0.0
padding_top: float = 0.0
padding_right: float = 0.0
[docs]
class ResponseStatus(Enum):
"""The overall result of a request."""
SUCCESS = 0
#: Something went wrong. That's all we know.
UNKNOWN_ERROR = 1
#: Something went wrong talking to the server. A 'Retry' may be apt.
COMMUNICATION_ERROR = 2
#: This requires the user to be signed in, and they aint.
NOT_SIGNED_IN_ERROR = 3
[docs]
@ioprepped
@dataclass
class Response(DocUIResponse):
"""Full docui response (v2)."""
page: Page
status: ResponseStatus = (
ResponseStatus.SUCCESS
)
#: The engine build this response was built for, as a sanity check:
#: responses can be tailored per-build (client-effect forms etc.),
#: so a consumer seeing a mismatch with its own build should treat
#: the response as stale (e.g. toss cached data) rather than use it.
for_build: int | None = None
#: Asset-package-versions the response's integer-indexed
#: language-strings resolve against (position = package index).
#: Present only on wire responses finalized to the indexed form by
#: the server; its presence declares the page + contained client
#: effects fully indexed (consumers may flag resource-form leaks),
#: and it doubles as the client's resolve/pre-warm manifest.
#: Locally-authored responses never carry it (indexing is a wire
#: compression; local pages stay in the authored resource form).
packages: list[str] = field(
default_factory=list
)
#: Effects to run on the client when this response is initially
#: received (not re-run on automatic page refreshes). Note that
#: effect payloads are not yet v2-native — text in them is
#: raw/legacy-lstr, pending clienteffect gaining a resolve-context
#: concept (see the SoundSpec followup in docs/followups.md).
#:
#: :meta private:
client_effects: list[clfx.Effect] = field(default_factory=list)
#: Local action to run after this response is initially received
#: (not re-run on automatic page refreshes). Will be handled by
#: :meth:`bauiv1lib.docui.DocUIController.local_action()`.
local_action: str | None = (
None
)
local_action_args: dict | None = None
#: New overall action to have the client schedule after this
#: response is received. Useful for redirecting to other pages or
#: closing the doc-ui window.
timed_action: Action | None = None
timed_action_delay: float = 0.0
#: If provided, error on builds older than this.
minimum_engine_build: int | None = None
#: Explicit shared-state id (defaults to the request path client-side).
shared_state_id: str | None = None
[docs]
@override
@classmethod
def get_type_id(cls) -> DocUIResponseTypeID:
return DocUIResponseTypeID.V2
# 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