wiring up inbox ui

This commit is contained in:
Eric Froemling 2024-12-06 22:46:44 -08:00
parent 8cbcfa771f
commit fbf5d420d4
No known key found for this signature in database
12 changed files with 680 additions and 133 deletions

106
.efrocachemap generated
View File

@ -421,44 +421,44 @@
"build/assets/ba_data/audio/zoeOw.ogg": "b2d705c31c9dcc1efdc71394764c3beb",
"build/assets/ba_data/audio/zoePickup01.ogg": "e9366dc2d2b8ab8b0c4e2c14c02d0789",
"build/assets/ba_data/audio/zoeScream01.ogg": "903e0e45ee9b3373e9d9ce20c814374e",
"build/assets/ba_data/data/langdata.json": "54a0a77dc0adb7ed5dd76cd175741fc7",
"build/assets/ba_data/data/languages/arabic.json": "4a6fc46285d6289ee14a7ccd9e801ac4",
"build/assets/ba_data/data/langdata.json": "03810e5cca79c5fa092f47648060ca0f",
"build/assets/ba_data/data/languages/arabic.json": "8f89f09ad168c251765efebde4c9069c",
"build/assets/ba_data/data/languages/belarussian.json": "1004e5ea10b8deaef517fd37e9309521",
"build/assets/ba_data/data/languages/chinese.json": "3c5e0a568008780f2e7258bf74b54efd",
"build/assets/ba_data/data/languages/chinese.json": "3a8ad6b99e13152872962019b3eef49d",
"build/assets/ba_data/data/languages/chinesetraditional.json": "904b35b656c53f9830e406565edd5120",
"build/assets/ba_data/data/languages/croatian.json": "1e541070309ff6be95b0c39940aa7e99",
"build/assets/ba_data/data/languages/czech.json": "d18b7d1c6bf51fc81af4084ef0e69e3e",
"build/assets/ba_data/data/languages/danish.json": "8e57db30c5250df2abff14a822f83ea7",
"build/assets/ba_data/data/languages/dutch.json": "734357560f53b4820221f6d60a0b79e8",
"build/assets/ba_data/data/languages/english.json": "dffc4a03b94c74f11da188a7c4187eda",
"build/assets/ba_data/data/languages/dutch.json": "f4e1e8e9231cda9d1bcc7e87a7f8821e",
"build/assets/ba_data/data/languages/english.json": "b5917c3b975155e35fedb655dbd7568c",
"build/assets/ba_data/data/languages/esperanto.json": "0e397cfa5f3fb8cef5f4a64f21cda880",
"build/assets/ba_data/data/languages/filipino.json": "b5c8fb4f820bb3b521516321d51f4523",
"build/assets/ba_data/data/languages/filipino.json": "08b626ee9d8b66c55e79a4829c6cb9f2",
"build/assets/ba_data/data/languages/french.json": "6d20655730b1017ef187fd828b91d43c",
"build/assets/ba_data/data/languages/german.json": "c979cb1397d53a1e5b6c9a7becf83072",
"build/assets/ba_data/data/languages/gibberish.json": "2efafa7c1d479ce1fa46e897739508e5",
"build/assets/ba_data/data/languages/german.json": "a150dbb5c0f43984757f7db295d96203",
"build/assets/ba_data/data/languages/gibberish.json": "df76e851aee59657b69e34efd54fee06",
"build/assets/ba_data/data/languages/greek.json": "d28d1092fbb00ed857cbd53124c0dc78",
"build/assets/ba_data/data/languages/hindi.json": "5f60453c7dd3853c95c6f822e92d5300",
"build/assets/ba_data/data/languages/hindi.json": "567e6976b3c72f891431ad7fcc62ab16",
"build/assets/ba_data/data/languages/hungarian.json": "9d88004a98f0fbe2ea72edd5e0b3002e",
"build/assets/ba_data/data/languages/indonesian.json": "2ccb3fe081ead7706dbebb1008a8bc4e",
"build/assets/ba_data/data/languages/italian.json": "3557cd4697da8c59ed33bda066e8cd93",
"build/assets/ba_data/data/languages/italian.json": "43735ea42d14c121bc14eace16f904a2",
"build/assets/ba_data/data/languages/korean.json": "4e3524327a0174250aff5e1ef4c0c597",
"build/assets/ba_data/data/languages/malay.json": "f6ce0426d03a62612e3e436ed5d1be1f",
"build/assets/ba_data/data/languages/persian.json": "2584895475fe62b3fe49a5ea5e69b4b1",
"build/assets/ba_data/data/languages/piratespeak.json": "b9fe871e6331b7178cbacbf7eb3033aa",
"build/assets/ba_data/data/languages/polish.json": "89333fb207f9eb2f22fff5a95b022c35",
"build/assets/ba_data/data/languages/portuguese.json": "e1c4414fced051d2c1967417fd47650a",
"build/assets/ba_data/data/languages/polish.json": "d0822d5d3bdd72ddb04dc3c43a0b1395",
"build/assets/ba_data/data/languages/portuguese.json": "46649f4a8f3c5f69758e8b75ffacf439",
"build/assets/ba_data/data/languages/romanian.json": "5ae206fe0b71c4015b02b86da8931c8f",
"build/assets/ba_data/data/languages/russian.json": "33c3943f1096aa37f9815d93c6ac1273",
"build/assets/ba_data/data/languages/russian.json": "72bdbb27ede61bbfeafbf81fa4a19e45",
"build/assets/ba_data/data/languages/serbian.json": "623fa4129a1154c2f32ed7867e56ff6a",
"build/assets/ba_data/data/languages/slovak.json": "3c08c748c96c71bd9e1d7291fb8817b6",
"build/assets/ba_data/data/languages/spanish.json": "27f564597977b8764583a10d750900be",
"build/assets/ba_data/data/languages/spanish.json": "13f587058931acbb68a48981063ee5ff",
"build/assets/ba_data/data/languages/swedish.json": "3b179e7333183c70adb0811246b09959",
"build/assets/ba_data/data/languages/tamil.json": "ead39b864228696a9b0d19344bc4b5ec",
"build/assets/ba_data/data/languages/thai.json": "383540a1e9c7c131ac579f51afc87471",
"build/assets/ba_data/data/languages/turkish.json": "9e8268786667aa3531593edb6ee66112",
"build/assets/ba_data/data/languages/ukrainian.json": "3a5b8132690fcd583d280879876c85b7",
"build/assets/ba_data/data/languages/turkish.json": "440cb59e69ed689018c17d4be0fb4696",
"build/assets/ba_data/data/languages/ukrainian.json": "6063d27c9d6ed013b2b64ff452433621",
"build/assets/ba_data/data/languages/venetian.json": "abebcc38ca2655578e65428cc0dd3c45",
"build/assets/ba_data/data/languages/vietnamese.json": "34a8b75acba2c0234e0b00fb4ef7d011",
"build/assets/ba_data/data/languages/vietnamese.json": "59f6686890ceac2b0ac92597751a18ca",
"build/assets/ba_data/data/maps/big_g.json": "1dd301d490643088a435ce75df971054",
"build/assets/ba_data/data/maps/bridgit.json": "6aea74805f4880cc11237c5734a24422",
"build/assets/ba_data/data/maps/courtyard.json": "4b836554c8949bcd2ae382f5e3c1a9cc",
@ -4099,42 +4099,42 @@
"build/assets/windows/Win32/ucrtbased.dll": "bfd1180c269d3950b76f35a63655e9e1",
"build/assets/windows/Win32/vc_redist.x86.exe": "15a5f1f876503885adbdf5b3989b3718",
"build/assets/windows/Win32/vcruntime140d.dll": "865b2af4d1e26a1a8073c89acb06e599",
"build/prefab/full/linux_arm64_gui/debug/ballisticakit": "c9c741a8c1908f709d9e69f95078e54d",
"build/prefab/full/linux_arm64_gui/release/ballisticakit": "48f6568ba24b78e376fcb0109d8811fa",
"build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "74f4a7a42a51aab41baa0b06933ba90a",
"build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "7c3305d87a435b8a2308faf9798af681",
"build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "f10715043bcd264941d66e63b00bb171",
"build/prefab/full/linux_x86_64_gui/release/ballisticakit": "701b28fdf672a88bdc85aac449a52933",
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "a9b00364c83414d86b81e99e5d9e1b10",
"build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "76e1077a37e919260846cfd96985bb3e",
"build/prefab/full/mac_arm64_gui/debug/ballisticakit": "fd6c8276e03e6af24c897d974838dc34",
"build/prefab/full/mac_arm64_gui/release/ballisticakit": "d5c311bb092286d8ef7f48f51e7da95f",
"build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "be9ae9fa1f04a87c4dfd319c02158b92",
"build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "bd15c256a1dd4e086564b23055904a68",
"build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "380d586848a9351eaff8da027b1d3aeb",
"build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "352089e2917fef437e176e148f5fcd91",
"build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "7667304055cd226341597c45534c812e",
"build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "b323fe4cdd3b7d2f31f6e05db7bb04f3",
"build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "e57cd21ff8c3566b1eb5fc9e79474d69",
"build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "206f0407a6d912b292c6530cd969030b",
"build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "e57cd21ff8c3566b1eb5fc9e79474d69",
"build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "206f0407a6d912b292c6530cd969030b",
"build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "6a891ea7e3609d5c30a3b321db59dc78",
"build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "7f37a6249393fc422a536b3e1622b96f",
"build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "6a891ea7e3609d5c30a3b321db59dc78",
"build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "7f37a6249393fc422a536b3e1622b96f",
"build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "209def6990e220110e5433581dad95e7",
"build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "910f66cc0ddf5d17e751dc295f84e63e",
"build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "209def6990e220110e5433581dad95e7",
"build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "910f66cc0ddf5d17e751dc295f84e63e",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "8cba3a66655dc40054d11acf13131226",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "a23295d8e71df2f8c308cfcd4b628827",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "72eaa6f4466fad2260c121711ec9ed09",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "c9748838494847186721a17dde8fd069",
"build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "e41041e152263ae5552eb0e25c6ae8b8",
"build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "a566b94b52cc4c281ada8a32a7fe078b",
"build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "604c937543f27648e66217818cfc9714",
"build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "d57aa1b54409ef038b2e86b1d59f9d26",
"build/prefab/full/linux_arm64_gui/debug/ballisticakit": "4fef5a35efc6a2e74b6238ec87838be4",
"build/prefab/full/linux_arm64_gui/release/ballisticakit": "9ff03f147a4b5effc838bc2539c17227",
"build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "455c1b9e6bba6db619756c25f6203b80",
"build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "27ab3c363db144716d818003bd7a18f9",
"build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "d9c645eb18380486349dd04f7fee4725",
"build/prefab/full/linux_x86_64_gui/release/ballisticakit": "15606ae17b5d02daa4fbc6549e8f1290",
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "fc798bac8c9e780695eabc930b605c2d",
"build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "43a57614eb5df0c52ee8e64c3061d47d",
"build/prefab/full/mac_arm64_gui/debug/ballisticakit": "7eec8b4ba4bb300f5f7389ca5a6abec3",
"build/prefab/full/mac_arm64_gui/release/ballisticakit": "5924800f1d7870bda557716722b2373b",
"build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "0ab737966bf957046ca15121b1beafbb",
"build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "f835b62e09a9933f5d4c98ba3c3195b1",
"build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "7e2c3b108f358e5dcee152f7acbba90f",
"build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "b2a7b275c79f8f8ea23b7ea1ec1bc3e6",
"build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "1d17fe8c07b5ec076f799e4e758354a1",
"build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "cb7eac5051dda9024db0076f7c7a0a64",
"build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "678f8cbc0a31cf59f86211804f3d18f3",
"build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "658a1267cf8454e74dd7a43d4921a862",
"build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "678f8cbc0a31cf59f86211804f3d18f3",
"build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "658a1267cf8454e74dd7a43d4921a862",
"build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "5dfc9ffb40df78765dee25eed63b4d30",
"build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "9c65f68f604202158a6c7486eeb985a9",
"build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "5dfc9ffb40df78765dee25eed63b4d30",
"build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "9c65f68f604202158a6c7486eeb985a9",
"build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "4e5e0cac71feae718dfcccdeaa03eba1",
"build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "c54e70ec873ecab76c655c20e13f595e",
"build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "4e5e0cac71feae718dfcccdeaa03eba1",
"build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "c54e70ec873ecab76c655c20e13f595e",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "131825eb39eb85e5231d1507cb03eee5",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "90f0217980b38b9068477208eff91a64",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "c7604ca233b96f41ee3d48e4d9841774",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "a8f0f4570ec807c71aa0aff1e9d02d40",
"build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "9fb98011f0afe0231bbee6357be07d11",
"build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "85cba33219b47db164d78d50b8b2edf5",
"build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "7e72e3c500358123aae35a73a4060171",
"build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "2476d7c95ae08ec0fb1662241fe27655",
"src/assets/ba_data/python/babase/_mgen/__init__.py": "f885fed7f2ed98ff2ba271f9dbe3391c",
"src/assets/ba_data/python/babase/_mgen/enums.py": "794d258d59fd17a61752843a9a0551ad",
"src/ballistica/base/mgen/pyembed/binding_base.inc": "06042d31df0ff9af96b99477162e2a91",

View File

@ -1,4 +1,4 @@
### 1.7.37 (build 22123, api 9, 2024-12-03)
### 1.7.37 (build 22125, api 9, 2024-12-06)
- Bumping api version to 9. As you'll see below, there's some UI changes that
will require a bit of work for any UI mods to adapt to. If your mods don't
touch UI stuff at all you can simply bump your api version and call it a day.

View File

@ -9,7 +9,7 @@ pur==7.3.2
pylint==3.3.2
pylsp-mypy==0.6.9
pytest==8.3.4
python-daemon==3.1.0
python-daemon==3.1.2
python-lsp-black==2.0.0
python-lsp-server==1.12.0
requests==2.32.3

View File

@ -252,6 +252,14 @@ if TYPE_CHECKING:
# type checking on both positional and keyword arguments (as of mypy
# 1.11).
# FIXME: Actually, currently (as of Dec 2024) mypy doesn't fully
# type check partial. The partial() call itself is checked, but the
# resulting callable seems to be essentially untyped. We should
# probably revise this stuff so that Call and WeakCall are for 100%
# complete calls so we can fully type check them using ParamSpecs or
# whatnot. We could then write a weak_partial() call if we actually
# need that particular combination of functionality.
# Note: Something here is wonky with pylint, possibly related to our
# custom pylint plugin. Disabling all checks seems to fix it.
# pylint: disable=all

View File

@ -205,6 +205,9 @@ class ClassicAppMode(babase.AppMode):
self, val: bacommon.cloud.ClassicAccountLiveData
) -> None:
achp = round(val.achievements / max(val.achievements_total, 1) * 100.0)
ibc = str(val.inbox_count)
if val.inbox_count_is_max:
ibc += '+'
_baclassic.set_root_ui_values(
tickets_text=str(val.tickets),
tokens_text=str(val.tokens),
@ -217,7 +220,7 @@ class ClassicAppMode(babase.AppMode):
achievements_percent_text=f'{achp}%',
level_text=str(val.level),
xp_text=f'{val.xp}/{val.xpmax}',
inbox_count_text=str(val.inbox_count),
inbox_count_text=ibc,
)
def _root_ui_menu_press(self) -> None:
@ -372,13 +375,16 @@ class ClassicAppMode(babase.AppMode):
)
def _root_ui_inbox_press(self) -> None:
from bauiv1lib.connectivity import wait_for_connectivity
from bauiv1lib.inbox import InboxWindow
self._auxiliary_window_nav(
win_type=InboxWindow,
win_create_call=lambda: InboxWindow(
origin_widget=bauiv1.get_special_widget('inbox_button')
),
wait_for_connectivity(
on_connected=lambda: self._auxiliary_window_nav(
win_type=InboxWindow,
win_create_call=lambda: InboxWindow(
origin_widget=bauiv1.get_special_widget('inbox_button')
),
)
)
def _root_ui_store_press(self) -> None:

View File

@ -53,7 +53,7 @@ if TYPE_CHECKING:
# Build number and version of the ballistica binary we expect to be
# using.
TARGET_BALLISTICA_BUILD = 22123
TARGET_BALLISTICA_BUILD = 22125
TARGET_BALLISTICA_VERSION = '1.7.37'

View File

@ -117,6 +117,24 @@ class CloudSubsystem(babase.AppSubsystem):
],
) -> None: ...
@overload
def send_message_cb(
self,
msg: bacommon.cloud.BSInboxRequestMessage,
on_response: Callable[
[bacommon.cloud.BSInboxRequestResponse | Exception], None
],
) -> None: ...
@overload
def send_message_cb(
self,
msg: bacommon.cloud.BSInboxEntryProcessMessage,
on_response: Callable[
[bacommon.cloud.BSInboxEntryProcessResponse | Exception], None
],
) -> None: ...
def send_message_cb(
self,
msg: Message,

View File

@ -4,22 +4,53 @@
from __future__ import annotations
import weakref
from dataclasses import dataclass
from typing import override
# from bauiv1lib.popup import PopupWindow
from efro.error import CommunicationError
import bacommon.cloud
import bauiv1 as bui
# Messages with format versions higher than this will show up as
# 'app needs to be updated to view this'
SUPPORTED_INBOX_MESSAGE_FORMAT_VERSION = 1
@dataclass
class _MessageEntry:
type: bacommon.cloud.BSInboxEntryType
id: str
height: float
text_height: float
scale: float
text: str
color: tuple[float, float, float]
backing: bui.Widget | None = None
button_positive: bui.Widget | None = None
button_negative: bui.Widget | None = None
message_text: bui.Widget | None = None
processing_complete: bool = False
class InboxWindow(bui.MainWindow):
"""Popup window to show account messages."""
def __del__(self) -> None:
print('~InboxWindow()')
def __init__(
self,
transition: str | None = 'in_right',
origin_widget: bui.Widget | None = None,
):
print('InboxWindow()')
assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale
self._message_entries: list[_MessageEntry] = []
self._width = 600 if uiscale is bui.UIScale.SMALL else 450
self._height = (
380
@ -83,7 +114,7 @@ class InboxWindow(bui.MainWindow):
h_align='center',
v_align='center',
scale=0.6,
text='INBOX (UNDER CONSTRUCTION)',
text=bui.Lstr(resource='inboxText'),
maxwidth=200,
color=bui.app.ui_v1.title_color,
)
@ -99,7 +130,9 @@ class InboxWindow(bui.MainWindow):
(110 if uiscale is bui.UIScale.SMALL else 30) + yoffs,
),
capture_arrows=True,
simple_culling_v=10,
simple_culling_v=200,
claims_left_right=True,
claims_up_down=True,
)
bui.widget(edit=self._scrollwidget, autoselect=True)
if uiscale is bui.UIScale.SMALL:
@ -109,32 +142,21 @@ class InboxWindow(bui.MainWindow):
)
bui.containerwidget(
edit=self._root_widget, cancel_button=self._back_button
edit=self._root_widget,
cancel_button=self._back_button,
single_depth=True,
)
entries: list[str] = []
incr = 20
sub_width = self._width - 90
sub_height = 40 + len(entries) * incr
# Kick off request.
plus = bui.app.plus
if plus is None or plus.accounts.primary is None:
self._error(bui.Lstr(resource='notSignedInText'))
return
self._subcontainer = bui.containerwidget(
parent=self._scrollwidget,
size=(sub_width, sub_height),
background=False,
)
for i, entry in enumerate(entries):
bui.textwidget(
parent=self._subcontainer,
position=(sub_width * 0.08 - 5, sub_height - 20 - incr * i),
maxwidth=20,
scale=0.5,
flatness=1.0,
shadow=0.0,
text=entry,
size=(0, 0),
h_align='right',
v_align='center',
with plus.accounts.primary:
plus.cloud.send_message_cb(
bacommon.cloud.BSInboxRequestMessage(),
on_response=bui.WeakCall(self._on_inbox_request_response),
)
@override
@ -147,16 +169,385 @@ class InboxWindow(bui.MainWindow):
)
)
# def _on_cancel_press(self) -> None:
# self._transition_out()
def _error(self, errmsg: bui.Lstr | str) -> None:
"""Put ourself in a permanent error state."""
bui.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height * 0.5),
maxwidth=self._width * 0.7,
scale=1.0,
flatness=1.0,
color=(1, 0, 0),
shadow=0.0,
text=errmsg,
size=(0, 0),
h_align='center',
v_align='center',
)
# def _transition_out(self) -> None:
# if not self._transitioning_out:
# self._transitioning_out = True
# bui.containerwidget(
# edit=self._root_widget, transition='out_scale')
def _on_message_entry_press(
self,
entry_weak: weakref.ReferenceType[_MessageEntry],
process_type: bacommon.cloud.BSInboxEntryProcessType,
) -> None:
entry = entry_weak()
if entry is None:
return
# @override
# def on_popup_cancel(self) -> None:
# bui.getsound('swish').play()
# self._transition_out()
self._neuter_message_entry(entry)
# We don't do anything for invalid messages.
if entry.type is bacommon.cloud.BSInboxEntryType.UNKNOWN:
entry.processing_complete = True
self._close_soon_if_all_processed()
return
# Error if we're somehow signed out now.
plus = bui.app.plus
if plus is None or plus.accounts.primary is None:
bui.screenmessage(
bui.Lstr(resource='notSignedInText'), color=(1, 0, 0)
)
bui.getsound('error').play()
return
# Message the master-server to process the entry.
with plus.accounts.primary:
plus.cloud.send_message_cb(
bacommon.cloud.BSInboxEntryProcessMessage(
entry.id, process_type
),
on_response=bui.WeakCall(
self._on_inbox_entry_process_response,
entry_weak,
process_type,
),
)
# Tweak the button to show this is in progress.
button = (
entry.button_positive
if process_type is bacommon.cloud.BSInboxEntryProcessType.POSITIVE
else entry.button_negative
)
if button is not None:
bui.buttonwidget(edit=button, label='...')
def _close_soon_if_all_processed(self) -> None:
bui.apptimer(0.25, bui.WeakCall(self._close_if_all_processed))
def _close_if_all_processed(self) -> None:
if not all(m.processing_complete for m in self._message_entries):
return
self.main_window_back()
def _neuter_message_entry(self, entry: _MessageEntry) -> None:
errsound = bui.getsound('error')
if entry.button_positive is not None:
bui.buttonwidget(
edit=entry.button_positive,
color=(0.5, 0.5, 0.5),
textcolor=(0.4, 0.4, 0.4),
on_activate_call=errsound.play,
)
if entry.button_negative is not None:
bui.buttonwidget(
edit=entry.button_negative,
color=(0.5, 0.5, 0.5),
textcolor=(0.4, 0.4, 0.4),
on_activate_call=errsound.play,
)
if entry.backing is not None:
bui.imagewidget(edit=entry.backing, color=(0.4, 0.4, 0.4))
if entry.message_text is not None:
bui.textwidget(edit=entry.message_text, color=(0.5, 0.5, 0.5, 0.5))
def _on_inbox_entry_process_response(
self,
entry_weak: weakref.ReferenceType[_MessageEntry],
process_type: bacommon.cloud.BSInboxEntryProcessType,
response: bacommon.cloud.BSInboxEntryProcessResponse | Exception,
) -> None:
entry = entry_weak()
if entry is None:
return
assert not entry.processing_complete
entry.processing_complete = True
self._close_soon_if_all_processed()
# No-op if our UI is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
# Tweak the button to show results.
button = (
entry.button_positive
if process_type is bacommon.cloud.BSInboxEntryProcessType.POSITIVE
else entry.button_negative
)
# See if we should show an error message.
if isinstance(response, Exception):
if isinstance(response, CommunicationError):
error_message = bui.Lstr(
resource='internal.unavailableNoConnectionText'
)
else:
error_message = bui.Lstr(resource='errorText')
elif response.error is not None:
error_message = bui.Lstr(
translate=('serverResponses', response.error)
)
else:
error_message = None
# Show error message if so.
if error_message is not None:
bui.screenmessage(error_message, color=(1, 0, 0))
bui.getsound('error').play()
if button is not None:
bui.buttonwidget(
edit=button, label=bui.Lstr(resource='errorText')
)
return
# Whee; no error. Mark as done.
if button is not None:
bui.buttonwidget(edit=button, label=bui.Lstr(resource='doneText'))
def _on_inbox_request_response(
self, response: bacommon.cloud.BSInboxRequestResponse | Exception
) -> None:
# pylint: disable=too-many-locals
# pylint: disable=too-many-statements
# pylint: disable=too-many-branches
# No-op if our UI is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
errmsg: str | bui.Lstr
if isinstance(response, Exception):
errmsg = bui.Lstr(resource='internal.unavailableNoConnectionText')
is_error = True
else:
is_error = response.error is not None
errmsg = (
''
if response.error is None
else bui.Lstr(translate=('serverResponses', response.error))
)
if is_error:
self._error(errmsg)
return
assert isinstance(response, bacommon.cloud.BSInboxRequestResponse)
# If we got no messages, don't touch anything. This keeps
# keyboard control working in the empty case.
if not response.entries:
bui.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height * 0.5),
maxwidth=self._width * 0.7,
scale=1.0,
flatness=1.0,
color=(0.4, 0.4, 0.4),
shadow=0.0,
text=bui.Lstr(resource='noMessagesText'),
size=(0, 0),
h_align='center',
v_align='center',
)
return
sub_width = self._width - 90
sub_height = 0.0
# Run the math on row heights/etc.
for i, entry in enumerate(response.entries):
# We need to flatten text here so we can measure it.
textfin: str
color: tuple[float, float, float]
# Messages with either newer formatting or unrecognized
# types show up as 'upgrade your app to see this'.
if (
entry.format_version > SUPPORTED_INBOX_MESSAGE_FORMAT_VERSION
or entry.type is bacommon.cloud.BSInboxEntryType.UNKNOWN
):
textfin = bui.Lstr(
translate=(
'serverResponses',
'You must update the app to view this.',
)
).evaluate()
color = (0.6, 0.6, 0.6)
else:
# Translate raw response and apply any replacements.
textfin = bui.Lstr(
translate=('serverResponses', entry.message)
).evaluate()
assert len(entry.subs) % 2 == 0 # Should always be even.
for j in range(0, len(entry.subs) - 1, 2):
textfin = textfin.replace(entry.subs[j], entry.subs[j + 1])
color = (0.55, 0.5, 0.7)
# Calc scale to fit width and then see what height we need
# at that scale.
t_width = max(
10.0, bui.get_string_width(textfin, suppress_warning=True)
)
scale = min(0.6, (sub_width * 0.9) / t_width)
t_height = (
max(10.0, bui.get_string_height(textfin, suppress_warning=True))
* scale
)
entry_height = 90.0 + t_height
self._message_entries.append(
_MessageEntry(
type=entry.type,
id=entry.id,
height=entry_height,
text_height=t_height,
scale=scale,
text=textfin,
color=color,
)
)
sub_height += entry_height
subcontainer = bui.containerwidget(
id='inboxsub',
parent=self._scrollwidget,
size=(sub_width, sub_height),
background=False,
single_depth=True,
claims_left_right=True,
claims_up_down=True,
)
backing_tex = bui.gettexture('buttonSquareWide')
buttonrows: list[list[bui.Widget]] = []
y = sub_height
for i, _entry in enumerate(response.entries):
message_entry = self._message_entries[i]
message_entry_weak = weakref.ref(message_entry)
bwidth = 140
bheight = 40
# Backing.
message_entry.backing = img = bui.imagewidget(
parent=subcontainer,
position=(-0.022 * sub_width, y - message_entry.height * 1.09),
texture=backing_tex,
size=(sub_width * 1.07, message_entry.height * 1.15),
color=message_entry.color,
opacity=0.9,
)
bui.widget(edit=img, depth_range=(0, 0.1))
buttonrow: list[bui.Widget] = []
have_negative_button = (
message_entry.type
is bacommon.cloud.BSInboxEntryType.CLAIM_DISCARD
)
message_entry.button_positive = btn = bui.buttonwidget(
parent=subcontainer,
position=(
(
(sub_width - bwidth - 25)
if have_negative_button
else ((sub_width - bwidth) * 0.5)
),
y - message_entry.height + 15.0,
),
size=(bwidth, bheight),
label=bui.Lstr(
resource=(
'claimText'
if message_entry.type
in {
bacommon.cloud.BSInboxEntryType.CLAIM,
bacommon.cloud.BSInboxEntryType.CLAIM_DISCARD,
}
else 'okText'
)
),
color=message_entry.color,
textcolor=(0, 1, 0),
on_activate_call=bui.WeakCall(
self._on_message_entry_press,
message_entry_weak,
bacommon.cloud.BSInboxEntryProcessType.POSITIVE,
),
)
bui.widget(edit=btn, depth_range=(0.1, 1.0))
buttonrow.append(btn)
if have_negative_button:
message_entry.button_negative = btn2 = bui.buttonwidget(
parent=subcontainer,
position=(25, y - message_entry.height + 15.0),
size=(bwidth, bheight),
label=bui.Lstr(resource='discardText'),
color=(0.85, 0.5, 0.7),
textcolor=(1, 0.4, 0.4),
on_activate_call=bui.WeakCall(
self._on_message_entry_press,
message_entry_weak,
bacommon.cloud.BSInboxEntryProcessType.NEGATIVE,
),
)
bui.widget(edit=btn2, depth_range=(0.1, 1.0))
buttonrow.append(btn2)
buttonrows.append(buttonrow)
message_entry.message_text = bui.textwidget(
parent=subcontainer,
position=(
sub_width * 0.5,
y - message_entry.text_height * 0.5 - 23.0,
),
scale=message_entry.scale,
flatness=1.0,
shadow=0.0,
text=message_entry.text,
size=(0, 0),
h_align='center',
v_align='center',
)
y -= message_entry.height
uiscale = bui.app.ui_v1.uiscale
above_widget = (
bui.get_special_widget('back_button')
if uiscale is bui.UIScale.SMALL
else self._back_button
)
assert above_widget is not None
for i, buttons in enumerate(buttonrows):
if i < len(buttonrows) - 1:
below_widget = buttonrows[i + 1][0]
else:
below_widget = None
assert buttons # We should never have an empty row.
for j, button in enumerate(buttons):
bui.widget(
edit=button,
up_widget=above_widget,
down_widget=(
button if below_widget is None else below_widget
),
right_widget=buttons[max(j - 1, 0)],
left_widget=buttons[min(j + 1, len(buttons) - 1)],
)
above_widget = buttons[0]

View File

@ -39,7 +39,7 @@ auto main(int argc, char** argv) -> int {
namespace ballistica {
// These are set automatically via script; don't modify them here.
const int kEngineBuildNumber = 22123;
const int kEngineBuildNumber = 22125;
const char* kEngineVersion = "1.7.37";
const int kEngineApiVersion = 9;

View File

@ -1115,6 +1115,7 @@ static auto PyContainerWidget(PyObject* self, PyObject* args, PyObject* keywds)
PyObject* click_activate_obj{Py_None};
PyObject* always_highlight_obj{Py_None};
PyObject* parent_obj{Py_None};
PyObject* id_obj{Py_None};
ContainerWidget* parent_widget;
PyObject* edit_obj{Py_None};
PyObject* selectable_obj{Py_None};
@ -1124,6 +1125,7 @@ static auto PyContainerWidget(PyObject* self, PyObject* args, PyObject* keywds)
static const char* kwlist[] = {"edit",
"parent",
"id",
"size",
"position",
"background",
@ -1156,16 +1158,16 @@ static auto PyContainerWidget(PyObject* self, PyObject* args, PyObject* keywds)
nullptr};
if (!PyArg_ParseTupleAndKeywords(
args, keywds, "|OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO",
const_cast<char**>(kwlist), &edit_obj, &parent_obj, &size_obj,
&pos_obj, &background_obj, &selected_child_obj, &transition_obj,
&cancel_button_obj, &start_button_obj, &root_selectable_obj,
&on_activate_call_obj, &claims_left_right_obj, &claims_tab_obj,
&selection_loops_obj, &selection_loops_to_parent_obj, &scale_obj,
&on_outside_click_call_obj, &single_depth_obj, &visible_child_obj,
&stack_offset_obj, &color_obj, &on_cancel_call_obj,
&print_list_exit_instructions_obj, &click_activate_obj,
&always_highlight_obj, &selectable_obj,
args, keywds, "|OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO",
const_cast<char**>(kwlist), &edit_obj, &parent_obj, &id_obj,
&size_obj, &pos_obj, &background_obj, &selected_child_obj,
&transition_obj, &cancel_button_obj, &start_button_obj,
&root_selectable_obj, &on_activate_call_obj, &claims_left_right_obj,
&claims_tab_obj, &selection_loops_obj, &selection_loops_to_parent_obj,
&scale_obj, &on_outside_click_call_obj, &single_depth_obj,
&visible_child_obj, &stack_offset_obj, &color_obj,
&on_cancel_call_obj, &print_list_exit_instructions_obj,
&click_activate_obj, &always_highlight_obj, &selectable_obj,
&scale_origin_stack_offset_obj, &toolbar_visibility_obj,
&on_select_call_obj, &claim_outside_clicks_obj,
&claims_up_down_obj)) {
@ -1187,6 +1189,9 @@ static auto PyContainerWidget(PyObject* self, PyObject* args, PyObject* keywds)
throw Exception("Invalid or nonexistent widget.",
PyExcType::kWidgetNotFound);
}
if (id_obj != Py_None) {
throw Exception("ID can only be set when creating.");
}
} else {
if (parent_obj == Py_None) {
BA_PRECONDITION(g_ui_v1 && g_ui_v1->screen_root_widget() != nullptr);
@ -1200,6 +1205,12 @@ static auto PyContainerWidget(PyObject* self, PyObject* args, PyObject* keywds)
PyExcType::kWidgetNotFound);
}
widget = Object::New<ContainerWidget>();
// Id needs to be set before adding to parent.
if (id_obj != Py_None) {
widget->set_id(Python::GetPyString(id_obj));
}
g_ui_v1->AddWidget(widget.get(), parent_widget);
}
@ -1386,6 +1397,7 @@ static PyMethodDef PyContainerWidgetDef = {
"containerwidget(*,\n"
" edit: bauiv1.Widget | None = None,\n"
" parent: bauiv1.Widget | None = None,\n"
" id: str | None = None,\n"
" size: Sequence[float] | None = None,\n"
" position: Sequence[float] | None = None,\n"
" background: bool | None = None,\n"
@ -2347,6 +2359,7 @@ static auto PyWidgetCall(PyObject* self, PyObject* args, PyObject* keywds)
PyObject* show_buffer_bottom_obj{Py_None};
PyObject* show_buffer_left_obj{Py_None};
PyObject* show_buffer_right_obj{Py_None};
PyObject* depth_range_obj{Py_None};
PyObject* autoselect_obj{Py_None};
static const char* kwlist[] = {"edit",
@ -2358,13 +2371,14 @@ static auto PyWidgetCall(PyObject* self, PyObject* args, PyObject* keywds)
"show_buffer_bottom",
"show_buffer_left",
"show_buffer_right",
"depth_range",
"autoselect",
nullptr};
if (!PyArg_ParseTupleAndKeywords(
args, keywds, "O|OOOOOOOOO", const_cast<char**>(kwlist), &edit_obj,
args, keywds, "O|OOOOOOOOOO", const_cast<char**>(kwlist), &edit_obj,
&up_widget_obj, &down_widget_obj, &left_widget_obj, &right_widget_obj,
&show_buffer_top_obj, &show_buffer_bottom_obj, &show_buffer_left_obj,
&show_buffer_right_obj, &autoselect_obj))
&show_buffer_right_obj, &depth_range_obj, &autoselect_obj))
return nullptr;
if (!g_base->CurrentContext().IsEmpty()) {
@ -2424,6 +2438,21 @@ static auto PyWidgetCall(PyObject* self, PyObject* args, PyObject* keywds)
if (show_buffer_right_obj != Py_None) {
widget->set_show_buffer_right(Python::GetPyFloat(show_buffer_right_obj));
}
if (depth_range_obj != Py_None) {
auto depth_range = Python::GetPyFloats(depth_range_obj);
if (depth_range.size() != 2) {
throw Exception("Expected 2 float values.", PyExcType::kValue);
}
if (depth_range[0] < 0.0 || depth_range[1] > 1.0
|| depth_range[1] <= depth_range[0]) {
throw Exception(
"Invalid depth range values;"
" values must be between 0 and 1 and second value must be larger "
"than first.",
PyExcType::kValue);
}
widget->set_depth_range(depth_range[0], depth_range[1]);
}
if (autoselect_obj != Py_None) {
widget->set_auto_select(Python::GetPyBool(autoselect_obj));
}
@ -2450,6 +2479,7 @@ static PyMethodDef PyWidgetDef = {
" show_buffer_bottom: float | None = None,\n"
" show_buffer_left: float | None = None,\n"
" show_buffer_right: float | None = None,\n"
" depth_range: tuple[float, float] | None = None,\n"
" autoselect: bool | None = None) -> None\n"
"\n"
"Edit common attributes of any widget.\n"

View File

@ -61,18 +61,18 @@ void ContainerWidget::DrawChildren(base::RenderPass* pass,
// We're expected to fill z space 0..1 when we draw... so we need to divide
// that space between our child widgets plus our bg layer.
float layer_thickness = 0.0f;
float layer_spacing = 0.0f;
float base_offset = 0.0f;
float layer_thickness1 = 0.0f;
float layer_thickness2 = 0.0f;
float layer_thickness3 = 0.0f;
float layer_spacing1 = 0.0f;
float layer_spacing2 = 0.0f;
float layer_spacing3 = 0.0f;
float base_offset1 = 0.0f;
float base_offset2 = 0.0f;
float base_offset3 = 0.0f;
float layer_thickness{};
float layer_spacing{};
float base_offset{};
float layer_thickness1{};
float layer_thickness2{};
float layer_thickness3{};
float layer_spacing1{};
float layer_spacing2{};
float layer_spacing3{};
float base_offset1{};
float base_offset2{};
float base_offset3{};
// In single-depth mode we draw all widgets at the same depth so they each get
// our full depth resolution. however they may overlap incorrectly.
@ -203,12 +203,17 @@ void ContainerWidget::DrawChildren(base::RenderPass* pass,
// Widgets can opt to use a subset of their allotted depth slice.
float d_min = w.depth_range_min();
float d_max = w.depth_range_max();
float this_z_offs;
float this_layer_thickness;
if (d_min != 0.0f || d_max != 1.0f) {
z_offs += layer_thickness * d_min;
layer_thickness *= (d_max - d_min);
this_z_offs = z_offs + layer_thickness * d_min;
this_layer_thickness = layer_thickness * (d_max - d_min);
} else {
this_z_offs = z_offs;
this_layer_thickness = layer_thickness;
}
c.Translate(x_offset + tx, y_offset + ty, z_offs);
c.Scale(s, s, layer_thickness);
c.Translate(x_offset + tx, y_offset + ty, this_z_offs);
c.Scale(s, s, this_layer_thickness);
c.Submit();
w.Draw(pass, draw_transparent);
}
@ -276,12 +281,17 @@ void ContainerWidget::DrawChildren(base::RenderPass* pass,
// Widgets can opt to use a subset of their allotted depth slice.
float d_min = w.depth_range_min();
float d_max = w.depth_range_max();
float this_z_offs;
float this_layer_thickness;
if (d_min != 0.0f || d_max != 1.0f) {
z_offs += layer_thickness * d_min;
layer_thickness *= (d_max - d_min);
this_z_offs = z_offs + layer_thickness * d_min;
this_layer_thickness = layer_thickness * (d_max - d_min);
} else {
this_z_offs = z_offs;
this_layer_thickness = layer_thickness;
}
c.Translate(x_offset + tx, y_offset + ty, z_offs);
c.Scale(s, s, layer_thickness);
c.Translate(x_offset + tx, y_offset + ty, this_z_offs);
c.Scale(s, s, this_layer_thickness);
c.Submit();
w.Draw(pass, draw_transparent);
}

View File

@ -3,9 +3,11 @@
"""Functionality related to cloud functionality."""
from __future__ import annotations
import datetime
from enum import Enum
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Annotated, override
from enum import Enum
from efro.message import Message, Response
from efro.dataclassio import ioprepped, IOAttrs
@ -355,3 +357,85 @@ class ClassicAccountLiveData:
xpmax: Annotated[int, IOAttrs('xpm')]
inbox_count: Annotated[int, IOAttrs('ibc')]
inbox_count_is_max: Annotated[bool, IOAttrs('ibcm')]
class BSInboxEntryType(Enum):
"""Types of entries that can be in an inbox."""
UNKNOWN = 'u' # Entry types we don't support will be this.
SIMPLE = 's'
CLAIM = 'c'
CLAIM_DISCARD = 'cd'
@ioprepped
@dataclass
class BSInboxEntry:
"""Single message in an inbox."""
type: Annotated[
BSInboxEntryType, IOAttrs('t', enum_fallback=BSInboxEntryType.UNKNOWN)
]
id: Annotated[str, IOAttrs('i')]
createtime: Annotated[datetime.datetime, IOAttrs('c')]
# If clients don't support format_version of a message they will
# display 'app needs to be updated to show this'.
format_version: Annotated[int, IOAttrs('f', soft_default=1)]
# These have soft defaults so can be removed in the future if desired.
message: Annotated[str, IOAttrs('m', soft_default='(invalid message)')]
subs: Annotated[list[str], IOAttrs('s', soft_default_factory=list)]
@ioprepped
@dataclass
class BSInboxRequestMessage(Message):
"""Message requesting our inbox."""
@override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [BSInboxRequestResponse]
@ioprepped
@dataclass
class BSInboxRequestResponse(Response):
"""Here's that inbox contents you asked for, boss."""
entries: Annotated[list[BSInboxEntry], IOAttrs('m')]
# Printable error if something goes wrong.
error: Annotated[str | None, IOAttrs('e')] = None
class BSInboxEntryProcessType(Enum):
"""Types of processing we can ask for."""
POSITIVE = 'p'
NEGATIVE = 'n'
@ioprepped
@dataclass
class BSInboxEntryProcessMessage(Message):
"""Do something to an inbox entry."""
id: Annotated[str, IOAttrs('i')]
process_type: Annotated[BSInboxEntryProcessType, IOAttrs('t')]
@override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [BSInboxEntryProcessResponse]
@ioprepped
@dataclass
class BSInboxEntryProcessResponse(Response):
"""Did something to that inbox entry, boss."""
# Printable error if something goes wrong.
error: Annotated[str | None, IOAttrs('e')] = None