1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-02-22 12:32:02 +00:00

WIP - fixes after rebase

This commit is contained in:
grdddj 2022-10-31 18:45:01 +01:00
parent de718e7778
commit e9f4d91118
211 changed files with 12064 additions and 2599 deletions

View File

@ -149,6 +149,39 @@ core fw btconly t1 build:
- trezor-fw-btconly-t1-*.*.*-$CI_COMMIT_SHORT_SHA.bin
expire_in: 1 week
core fw R btconly debug build:
stage: build
<<: *gitlab_caching
needs: []
variables:
TREZOR_MODEL: "R"
BITCOIN_ONLY: "1"
PYOPT: "0"
script:
- nix-shell --run "poetry run make -C core build_firmware"
- cp core/build/firmware/firmware.bin trezor-fw-btconly-debug-tr-$CORE_VERSION-$CI_COMMIT_SHORT_SHA.bin
artifacts:
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
paths:
- trezor-fw-btconly-*.*.*-$CI_COMMIT_SHORT_SHA.bin
expire_in: 1 week
core fw R btconly build:
stage: build
<<: *gitlab_caching
needs: []
variables:
TREZOR_MODEL: "R"
BITCOIN_ONLY: "1"
script:
- nix-shell --run "poetry run make -C core build_firmware"
- cp core/build/firmware/firmware.bin trezor-fw-btconly-tr-$CORE_VERSION-$CI_COMMIT_SHORT_SHA.bin
artifacts:
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
paths:
- trezor-fw-btconly-*.*.*-$CI_COMMIT_SHORT_SHA.bin
expire_in: 1 week
# Non-frozen emulator build. This means you still need Python files
# present which get interpreted.
core unix regular build:
@ -278,6 +311,40 @@ core unix frozen R debug build:
artifacts:
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
untracked: true
expire_in: 10 weeks
core unix frozen R debug build arm:
image: nixos/nix
stage: build
<<: *gitlab_caching
needs: []
variables:
PYOPT: "0"
TREZOR_MODEL: "R"
script:
- nix-shell --run "poetry run make -C core build_unix_frozen"
- mv core/build/unix/trezor-emu-core core/build/unix/trezor-emu-core-arm
artifacts:
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
untracked: true
expire_in: 10 weeks
tags:
- docker_darwin_arm
# Debugger build for gdb/lldb.
core unix R debugger build:
stage: build
<<: *gitlab_caching
needs: []
variables:
TREZOR_EMULATOR_FROZEN: "1"
TREZOR_MODEL: "R"
script:
- nix-shell --run "poetry run make -C core build_unix_debug"
artifacts:
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
paths:
- core/build/unix
expire_in: 1 week
core unix frozen debug asan build:

View File

@ -265,6 +265,25 @@ ui tests ui2 fixtures deploy:
tags:
- deploy
ui tests R fixtures deploy:
stage: deploy
variables:
DEPLOY_PATH: "${DEPLOY_BASE_DIR}/ui_tests/"
BUCKET: "data.trezor.io"
GIT_SUBMODULE_STRATEGY: "none"
before_script: [] # no poetry
needs:
- core device R test
script:
- echo "Deploying to $DEPLOY_PATH"
- rsync --delete -va ci/ui_test_records/* "$DEPLOY_PATH"
- source ${AWS_DEPLOY_DATA}
- aws s3 sync $DEPLOY_PATH s3://$BUCKET/dev/firmware/ui_tests
# This "hack" is needed because aws does not have an easy option to generate autoindex. We fetch the one autogenerated by nginx on local server.
- wget https://firmware.corp.sldev.cz/ui_tests/ -O index.html && aws s3 cp index.html s3://$BUCKET/dev/firmware/ui_tests/
tags:
- deploy
# sync to aws
sync emulators to aws:

View File

@ -11,14 +11,14 @@ from tests.ui_tests import read_fixtures # isort:skip
read_fixtures()
from tests.ui_tests import _hash_files, FILE_HASHES, SCREENS_DIR # isort:skip
# As in CI we are running T1 and TT tests separately, there will
# always be the other model missing.
# As in CI we are running tests for more models separately,
# there will always be the other models missing.
# Therefore, choosing just the cases for our model.
if len(sys.argv) > 1 and sys.argv[1].upper() == "T1":
model = "T1"
if len(sys.argv) > 1:
model = sys.argv[1].upper()
else:
model = "TT"
if os.getenv("UI2") == "1":
if model == "TT" and os.getenv("UI2") == "1":
model += "ui2"
model_file_hashes = {k: v for k, v in FILE_HASHES.items() if k.startswith(f"{model}_")}

View File

@ -152,7 +152,7 @@ core device R test:
- tests/trezor.log
- master_diff
when: always
expire_in: 1 week
expire_in: 4 weeks
reports:
junit: tests/junit.xml

View File

@ -28,6 +28,7 @@ message DebugLinkDecision {
DOWN = 1;
LEFT = 2;
RIGHT = 3;
ALL_THE_WAY_UP = 4;
}
/**
@ -39,10 +40,21 @@ message DebugLinkDecision {
INFO = 2;
}
/**
* Structure representing model R button presses
*/
// TODO: probably delete the middle_btn as it is not a physical one
enum DebugPhysicalButton {
LEFT_BTN = 0;
MIDDLE_BTN = 1;
RIGHT_BTN = 2;
}
optional uint32 x = 4; // touch X coordinate
optional uint32 y = 5; // touch Y coordinate
optional bool wait = 6; // wait for layout change
optional uint32 hold_ms = 7; // touch hold duration
optional DebugPhysicalButton physical_button = 8; // physical button press
}
/**

View File

@ -306,6 +306,7 @@ message ResetDevice {
optional bool skip_backup = 8; // postpone seed backup to BackupDevice workflow
optional bool no_backup = 9; // indicate that no backup is going to be made
optional BackupType backup_type = 10 [default=Bip39]; // type of the mnemonic backup
optional bool show_tutorial = 11 [default=true]; // whether to show device tutorial
}
/**
@ -348,6 +349,7 @@ message RecoveryDevice {
optional RecoveryDeviceType type = 8; // supported recovery type
optional uint32 u2f_counter = 9; // U2F counter
optional bool dry_run = 10; // perform dry-run recovery workflow (for safe mnemonic validation)
optional bool show_tutorial = 11 [default=true]; // whether to show device tutorial
/**
* Type of recovery procedure. These should be used as bitmask, e.g.,
* `RecoveryDeviceType_ScrambledWords | RecoveryDeviceType_Matrix`

1
core/.gitignore vendored
View File

@ -8,3 +8,4 @@ tests/trezor_monero_tests*
.coverage.*
htmlcov/
mypy_report
tools/gdb_scripts/*.log

View File

@ -23,6 +23,7 @@ PYOPT ?= 1
BITCOIN_ONLY ?= 0
TREZOR_MODEL ?= T
TREZOR_MEMPERF ?= 0
TREZOR_EMULATOR_FROZEN ?= 0
ADDRESS_SANITIZER ?= 0
UI2 ?= 0
@ -84,7 +85,7 @@ test: ## run unit tests
cd tests ; ./run_tests.sh $(TESTOPTS)
test_rust: ## run rs unit tests
cd embed/rust ; cargo test --target=$(RUST_TARGET) --no-default-features --features model_t$(shell echo $(TREZOR_MODEL) | tr "TR" "tr"),test -- --test-threads=1
cd embed/rust ; cargo test $(TESTOPTS) --target=$(RUST_TARGET) --no-default-features --features model_t$(shell echo $(TREZOR_MODEL) | tr "TR" "tr"),test -- --test-threads=1 --nocapture
test_emu: ## run selected device tests from python-trezor
$(EMU_TEST) $(PYTEST) $(TESTPATH)/device_tests $(TESTOPTS)
@ -174,7 +175,7 @@ build_unix_frozen: templates build_cross ## build unix port with frozen modules
$(SCONS) CFLAGS="$(CFLAGS)" $(UNIX_BUILD_DIR)/trezor-emu-core $(UNIX_PORT_OPTS) TREZOR_MODEL="$(TREZOR_MODEL)" PYOPT="$(PYOPT)" BITCOIN_ONLY="$(BITCOIN_ONLY)" TREZOR_EMULATOR_ASAN="$(ADDRESS_SANITIZER)" UI2="$(UI2)" TREZOR_MEMPERF="$(TREZOR_MEMPERF)" TREZOR_EMULATOR_FROZEN=1
build_unix_debug: templates ## build unix port
$(SCONS) --max-drift=1 CFLAGS="$(CFLAGS)" $(UNIX_BUILD_DIR)/trezor-emu-core $(UNIX_PORT_OPTS) TREZOR_MODEL="$(TREZOR_MODEL)" BITCOIN_ONLY="$(BITCOIN_ONLY)" TREZOR_EMULATOR_ASAN=1 UI2="$(UI2)" TREZOR_EMULATOR_DEBUGGABLE=1
$(SCONS) --max-drift=1 CFLAGS="$(CFLAGS)" $(UNIX_BUILD_DIR)/trezor-emu-core $(UNIX_PORT_OPTS) TREZOR_MODEL="$(TREZOR_MODEL)" PYOPT="0" BITCOIN_ONLY="$(BITCOIN_ONLY)" TREZOR_EMULATOR_ASAN=1 UI2="$(UI2)" TREZOR_EMULATOR_DEBUGGABLE=1 TREZOR_EMULATOR_FROZEN="$(TREZOR_EMULATOR_FROZEN)"
build_cross: ## build mpy-cross port
$(MAKE) -C vendor/micropython/mpy-cross $(CROSS_PORT_OPTS)

View File

@ -613,8 +613,14 @@ if FROZEN:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/t1.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/t1.py'))
elif TREZOR_MODEL in ('R',):
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/tr/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/tr.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tr/__init__.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tr/reset.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tr/recovery.py'))
if EVERYTHING:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tr/webauthn.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tr/altcoin.py'))
else:
raise ValueError('Unknown Trezor model')
@ -643,7 +649,20 @@ if FROZEN:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/common/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/common/*/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/debug/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/homescreen/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/homescreen/*.py',
# Each homescreen gets included individually below, based on model
exclude=[
SOURCE_PY_DIR + 'apps/homescreen/tt.py',
SOURCE_PY_DIR + 'apps/homescreen/tr.py',
SOURCE_PY_DIR + 'apps/homescreen/t1.py',
]
))
if TREZOR_MODEL in ('T',):
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/homescreen/tt.py'))
elif TREZOR_MODEL in ('R',):
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/homescreen/tr.py'))
elif TREZOR_MODEL in ('1',):
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/homescreen/t1.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/management/*.py',
exclude=[
SOURCE_PY_DIR + 'apps/management/sd_protect.py',

View File

@ -567,6 +567,7 @@ if FROZEN:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/t1.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/t1.py'))
elif TREZOR_MODEL in ('R',):
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/tr/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/tr.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tr/__init__.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tr/reset.py'))
@ -602,7 +603,20 @@ if FROZEN:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/common/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/common/*/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/debug/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/homescreen/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/homescreen/*.py',
# Each homescreen gets included individually below, based on model
exclude=[
SOURCE_PY_DIR + 'apps/homescreen/tt.py',
SOURCE_PY_DIR + 'apps/homescreen/tr.py',
SOURCE_PY_DIR + 'apps/homescreen/t1.py',
]
))
if TREZOR_MODEL in ('T',):
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/homescreen/tt.py'))
elif TREZOR_MODEL in ('R',):
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/homescreen/tr.py'))
elif TREZOR_MODEL in ('1',):
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/homescreen/t1.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/management/*.py',
exclude=[
SOURCE_PY_DIR + 'apps/management/sd_protect.py',

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 B

BIN
core/assets/model_r/bin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 B

View File

@ -86,6 +86,7 @@ STATIC const mp_rom_map_elem_t mp_module_trezorio_globals_table[] = {
{MP_ROM_QSTR(MP_QSTR_TOUCH_START), MP_ROM_INT((TOUCH_START >> 24) & 0xFFU)},
{MP_ROM_QSTR(MP_QSTR_TOUCH_MOVE), MP_ROM_INT((TOUCH_MOVE >> 24) & 0xFFU)},
{MP_ROM_QSTR(MP_QSTR_TOUCH_END), MP_ROM_INT((TOUCH_END >> 24) & 0xFFU)},
#elif defined TREZOR_MODEL_1 || defined TREZOR_MODEL_R
{MP_ROM_QSTR(MP_QSTR_BUTTON), MP_ROM_INT(BUTTON_IFACE)},
{MP_ROM_QSTR(MP_QSTR_BUTTON_PRESSED),
@ -94,8 +95,11 @@ STATIC const mp_rom_map_elem_t mp_module_trezorio_globals_table[] = {
MP_ROM_INT((BTN_EVT_UP >> 24) & 0x3U)},
{MP_ROM_QSTR(MP_QSTR_BUTTON_LEFT), MP_ROM_INT(BTN_LEFT)},
{MP_ROM_QSTR(MP_QSTR_BUTTON_RIGHT), MP_ROM_INT(BTN_RIGHT)},
#endif
{MP_ROM_QSTR(MP_QSTR_USB_CHECK), MP_ROM_INT(USB_DATA_IFACE)},
{MP_ROM_QSTR(MP_QSTR_FlashOTP), MP_ROM_PTR(&mod_trezorio_FlashOTP_type)},
{MP_ROM_QSTR(MP_QSTR_USB), MP_ROM_PTR(&mod_trezorio_USB_type)},

View File

@ -0,0 +1,203 @@
#include <stdint.h>
// clang-format off
// - the first two bytes are width and height of the glyph
// - the third, fourth and fifth bytes are advance, bearingX and bearingY of the horizontal metrics of the glyph
// - the rest is packed 1-bit glyph data
/* */ static const uint8_t Font_Unifont_Bold_16_glyph_32[] = { 0, 0, 7, 0, 0 }; // hand-changed to 7 to have 9px space between words
/* ! */ static const uint8_t Font_Unifont_Bold_16_glyph_33[] = { 2, 10, 7, 2, 10, 255, 252, 240 };
/* " */ static const uint8_t Font_Unifont_Bold_16_glyph_34[] = { 6, 4, 7, 0, 12, 207, 60, 209, 0 };
/* # */ static const uint8_t Font_Unifont_Bold_16_glyph_35[] = { 7, 10, 8, 0, 10, 54, 108, 223, 246, 205, 191, 236, 217, 176 };
/* $ */ static const uint8_t Font_Unifont_Bold_16_glyph_36[] = { 7, 10, 8, 0, 10, 16, 251, 94, 183, 3, 181, 235, 124, 32 };
/* % */ static const uint8_t Font_Unifont_Bold_16_glyph_37[] = { 7, 10, 8, 0, 10, 97, 102, 211, 65, 2, 11, 43, 150, 24 };
/* & */ static const uint8_t Font_Unifont_Bold_16_glyph_38[] = { 8, 10, 8, 0, 10, 56, 108, 108, 104, 48, 107, 206, 204, 206, 122, 0 };
/* ' */ static const uint8_t Font_Unifont_Bold_16_glyph_39[] = { 2, 4, 7, 2, 12, 253, 0 };
/* ( */ static const uint8_t Font_Unifont_Bold_16_glyph_40[] = { 4, 12, 7, 2, 11, 54, 108, 204, 204, 198, 99, 0 };
/* ) */ static const uint8_t Font_Unifont_Bold_16_glyph_41[] = { 4, 12, 7, 0, 11, 198, 99, 51, 51, 54, 108, 0 };
/* * */ static const uint8_t Font_Unifont_Bold_16_glyph_42[] = { 7, 7, 8, 0, 8, 17, 37, 81, 197, 82, 68, 0 };
/* + */ static const uint8_t Font_Unifont_Bold_16_glyph_43[] = { 7, 7, 8, 0, 8, 16, 32, 71, 241, 2, 4, 0 };
/* , */ static const uint8_t Font_Unifont_Bold_16_glyph_44[] = { 3, 4, 7, 1, 2, 237, 224 };
/* - */ static const uint8_t Font_Unifont_Bold_16_glyph_45[] = { 5, 1, 7, 0, 5, 248 };
/* . */ static const uint8_t Font_Unifont_Bold_16_glyph_46[] = { 2, 2, 7, 2, 2, 240 };
/* / */ static const uint8_t Font_Unifont_Bold_16_glyph_47[] = { 6, 10, 7, 0, 10, 12, 49, 132, 48, 194, 24, 195, 0 };
/* 0 */ static const uint8_t Font_Unifont_Bold_16_glyph_48[] = { 7, 10, 8, 0, 10, 56, 219, 30, 125, 122, 249, 227, 108, 112 };
/* 1 */ static const uint8_t Font_Unifont_Bold_16_glyph_49[] = { 6, 10, 7, 0, 10, 51, 195, 12, 48, 195, 12, 51, 240 };
/* 2 */ static const uint8_t Font_Unifont_Bold_16_glyph_50[] = { 7, 10, 8, 0, 10, 125, 143, 24, 48, 195, 12, 112, 193, 252 };
/* 3 */ static const uint8_t Font_Unifont_Bold_16_glyph_51[] = { 7, 10, 8, 0, 10, 125, 143, 24, 49, 192, 193, 227, 198, 248 };
/* 4 */ static const uint8_t Font_Unifont_Bold_16_glyph_52[] = { 7, 10, 8, 0, 10, 12, 120, 179, 100, 217, 191, 134, 12, 24 };
/* 5 */ static const uint8_t Font_Unifont_Bold_16_glyph_53[] = { 7, 10, 8, 0, 10, 255, 131, 6, 15, 193, 193, 227, 198, 248 };
/* 6 */ static const uint8_t Font_Unifont_Bold_16_glyph_54[] = { 7, 10, 8, 0, 10, 60, 195, 6, 15, 216, 241, 227, 198, 248 };
/* 7 */ static const uint8_t Font_Unifont_Bold_16_glyph_55[] = { 6, 10, 7, 0, 10, 252, 48, 195, 24, 99, 12, 97, 128 };
/* 8 */ static const uint8_t Font_Unifont_Bold_16_glyph_56[] = { 7, 10, 8, 0, 10, 125, 143, 30, 55, 216, 241, 227, 198, 248 };
/* 9 */ static const uint8_t Font_Unifont_Bold_16_glyph_57[] = { 7, 10, 8, 0, 10, 125, 143, 30, 60, 111, 193, 131, 12, 240 };
/* : */ static const uint8_t Font_Unifont_Bold_16_glyph_58[] = { 2, 7, 7, 2, 8, 240, 60 };
/* ; */ static const uint8_t Font_Unifont_Bold_16_glyph_59[] = { 2, 9, 7, 2, 8, 240, 61, 128 };
/* < */ static const uint8_t Font_Unifont_Bold_16_glyph_60[] = { 6, 9, 7, 0, 9, 12, 99, 24, 193, 131, 6, 12 };
/* = */ static const uint8_t Font_Unifont_Bold_16_glyph_61[] = { 6, 5, 7, 0, 7, 252, 0, 0, 252 };
/* > */ static const uint8_t Font_Unifont_Bold_16_glyph_62[] = { 6, 9, 7, 0, 9, 193, 131, 6, 12, 99, 24, 192 };
/* ? */ static const uint8_t Font_Unifont_Bold_16_glyph_63[] = { 7, 10, 8, 0, 10, 125, 143, 24, 48, 195, 6, 0, 24, 48 };
/* @ */ static const uint8_t Font_Unifont_Bold_16_glyph_64[] = { 7, 10, 8, 0, 10, 60, 134, 109, 187, 118, 237, 205, 64, 124 };
/* A */ static const uint8_t Font_Unifont_Bold_16_glyph_65[] = { 7, 10, 8, 0, 10, 56, 249, 182, 60, 120, 255, 227, 199, 140 };
/* B */ static const uint8_t Font_Unifont_Bold_16_glyph_66[] = { 7, 10, 8, 0, 10, 253, 143, 30, 63, 216, 241, 227, 199, 248 };
/* C */ static const uint8_t Font_Unifont_Bold_16_glyph_67[] = { 7, 10, 8, 0, 10, 125, 143, 30, 12, 24, 48, 99, 198, 248 };
/* D */ static const uint8_t Font_Unifont_Bold_16_glyph_68[] = { 7, 10, 8, 0, 10, 241, 155, 30, 60, 120, 241, 227, 205, 224 };
/* E */ static const uint8_t Font_Unifont_Bold_16_glyph_69[] = { 6, 10, 7, 0, 10, 255, 12, 48, 251, 12, 48, 195, 240 };
/* F */ static const uint8_t Font_Unifont_Bold_16_glyph_70[] = { 6, 10, 7, 0, 10, 255, 12, 48, 195, 236, 48, 195, 0 };
/* G */ static const uint8_t Font_Unifont_Bold_16_glyph_71[] = { 7, 10, 8, 0, 10, 125, 143, 30, 12, 27, 241, 227, 206, 236 };
/* H */ static const uint8_t Font_Unifont_Bold_16_glyph_72[] = { 7, 10, 8, 0, 10, 199, 143, 30, 63, 248, 241, 227, 199, 140 };
/* I */ static const uint8_t Font_Unifont_Bold_16_glyph_73[] = { 6, 10, 7, 0, 10, 252, 195, 12, 48, 195, 12, 51, 240 };
/* J */ static const uint8_t Font_Unifont_Bold_16_glyph_74[] = { 7, 10, 8, 0, 10, 62, 24, 48, 96, 193, 131, 102, 204, 240 };
/* K */ static const uint8_t Font_Unifont_Bold_16_glyph_75[] = { 7, 10, 8, 0, 10, 199, 143, 54, 207, 28, 62, 110, 207, 140 };
/* L */ static const uint8_t Font_Unifont_Bold_16_glyph_76[] = { 6, 10, 7, 0, 10, 195, 12, 48, 195, 12, 48, 195, 240 };
/* M */ static const uint8_t Font_Unifont_Bold_16_glyph_77[] = { 7, 10, 8, 0, 10, 131, 143, 31, 127, 250, 245, 227, 199, 140 };
/* N */ static const uint8_t Font_Unifont_Bold_16_glyph_78[] = { 7, 10, 8, 0, 10, 199, 207, 158, 189, 122, 245, 231, 207, 140 };
/* O */ static const uint8_t Font_Unifont_Bold_16_glyph_79[] = { 7, 10, 8, 0, 10, 125, 143, 30, 60, 120, 241, 227, 198, 248 };
/* P */ static const uint8_t Font_Unifont_Bold_16_glyph_80[] = { 7, 10, 8, 0, 10, 253, 143, 30, 60, 127, 176, 96, 193, 128 };
/* Q */ static const uint8_t Font_Unifont_Bold_16_glyph_81[] = { 7, 11, 8, 0, 10, 125, 143, 30, 60, 120, 241, 235, 238, 112, 24 };
/* R */ static const uint8_t Font_Unifont_Bold_16_glyph_82[] = { 7, 10, 8, 0, 10, 253, 143, 30, 60, 127, 182, 102, 199, 140 };
/* S */ static const uint8_t Font_Unifont_Bold_16_glyph_83[] = { 7, 10, 8, 0, 10, 125, 143, 31, 7, 135, 131, 227, 198, 248 };
/* T */ static const uint8_t Font_Unifont_Bold_16_glyph_84[] = { 7, 10, 8, 0, 10, 254, 48, 96, 193, 131, 6, 12, 24, 48 };
/* U */ static const uint8_t Font_Unifont_Bold_16_glyph_85[] = { 7, 10, 8, 0, 10, 199, 143, 30, 60, 120, 241, 227, 238, 248 };
/* V */ static const uint8_t Font_Unifont_Bold_16_glyph_86[] = { 7, 10, 8, 0, 10, 199, 143, 26, 38, 205, 155, 20, 40, 112 };
/* W */ static const uint8_t Font_Unifont_Bold_16_glyph_87[] = { 7, 10, 8, 0, 10, 199, 143, 30, 189, 122, 245, 255, 238, 136 };
/* X */ static const uint8_t Font_Unifont_Bold_16_glyph_88[] = { 7, 10, 8, 0, 10, 199, 141, 179, 99, 135, 27, 54, 199, 140 };
/* Y */ static const uint8_t Font_Unifont_Bold_16_glyph_89[] = { 6, 10, 7, 0, 10, 207, 60, 243, 73, 227, 12, 48, 192 };
/* Z */ static const uint8_t Font_Unifont_Bold_16_glyph_90[] = { 7, 10, 8, 0, 10, 254, 12, 56, 225, 135, 28, 112, 193, 252 };
/* [ */ static const uint8_t Font_Unifont_Bold_16_glyph_91[] = { 4, 12, 7, 2, 11, 252, 204, 204, 204, 204, 207, 0 };
/* \ */ static const uint8_t Font_Unifont_Bold_16_glyph_92[] = { 6, 10, 7, 0, 10, 195, 6, 8, 48, 193, 6, 12, 48 };
/* ] */ static const uint8_t Font_Unifont_Bold_16_glyph_93[] = { 4, 12, 7, 0, 11, 243, 51, 51, 51, 51, 63, 0 };
/* ^ */ static const uint8_t Font_Unifont_Bold_16_glyph_94[] = { 7, 3, 8, 0, 12, 56, 219, 24 };
/* _ */ static const uint8_t Font_Unifont_Bold_16_glyph_95[] = { 7, 1, 7, 0, 0, 254 };
/* ` */ static const uint8_t Font_Unifont_Bold_16_glyph_96[] = { 4, 3, 7, 0, 13, 198, 48 };
/* a */ static const uint8_t Font_Unifont_Bold_16_glyph_97[] = { 7, 8, 8, 0, 8, 125, 140, 27, 252, 120, 243, 187, 0 };
/* b */ static const uint8_t Font_Unifont_Bold_16_glyph_98[] = { 7, 11, 8, 0, 11, 193, 131, 6, 238, 120, 241, 227, 199, 207, 112 };
/* c */ static const uint8_t Font_Unifont_Bold_16_glyph_99[] = { 7, 8, 8, 0, 8, 125, 143, 30, 12, 24, 241, 190, 0 };
/* d */ static const uint8_t Font_Unifont_Bold_16_glyph_100[] = { 7, 11, 8, 0, 11, 6, 12, 25, 188, 248, 241, 227, 199, 156, 216 };
/* e */ static const uint8_t Font_Unifont_Bold_16_glyph_101[] = { 7, 8, 8, 0, 8, 125, 143, 31, 252, 24, 241, 190, 0 };
/* f */ static const uint8_t Font_Unifont_Bold_16_glyph_102[] = { 7, 11, 8, 0, 11, 30, 96, 193, 143, 230, 12, 24, 48, 97, 240 };
/* g */ static const uint8_t Font_Unifont_Bold_16_glyph_103[] = { 7, 11, 8, 0, 9, 2, 247, 54, 108, 207, 8, 62, 207, 141, 240 };
/* h */ static const uint8_t Font_Unifont_Bold_16_glyph_104[] = { 7, 11, 8, 0, 11, 193, 131, 6, 238, 120, 241, 227, 199, 143, 24 };
/* i */ static const uint8_t Font_Unifont_Bold_16_glyph_105[] = { 6, 11, 7, 0, 11, 48, 192, 60, 48, 195, 12, 48, 207, 192 };
/* j */ static const uint8_t Font_Unifont_Bold_16_glyph_106[] = { 6, 13, 8, 0, 11, 24, 96, 31, 12, 48, 195, 12, 60, 246, 112 };
/* k */ static const uint8_t Font_Unifont_Bold_16_glyph_107[] = { 7, 11, 8, 0, 11, 193, 131, 6, 60, 251, 60, 120, 217, 159, 24 };
/* l */ static const uint8_t Font_Unifont_Bold_16_glyph_108[] = { 6, 11, 7, 0, 11, 240, 195, 12, 48, 195, 12, 48, 207, 192 };
/* m */ static const uint8_t Font_Unifont_Bold_16_glyph_109[] = { 7, 8, 8, 0, 8, 237, 175, 94, 189, 122, 245, 235, 0 };
/* n */ static const uint8_t Font_Unifont_Bold_16_glyph_110[] = { 7, 8, 8, 0, 8, 221, 207, 30, 60, 120, 241, 227, 0 };
/* o */ static const uint8_t Font_Unifont_Bold_16_glyph_111[] = { 7, 8, 8, 0, 8, 125, 143, 30, 60, 120, 241, 190, 0 };
/* p */ static const uint8_t Font_Unifont_Bold_16_glyph_112[] = { 7, 10, 8, 0, 8, 221, 207, 30, 60, 120, 249, 238, 193, 128 };
/* q */ static const uint8_t Font_Unifont_Bold_16_glyph_113[] = { 7, 10, 8, 0, 8, 119, 159, 30, 60, 120, 243, 187, 6, 12 };
/* r */ static const uint8_t Font_Unifont_Bold_16_glyph_114[] = { 7, 8, 8, 0, 8, 221, 207, 30, 12, 24, 48, 96, 0 };
/* s */ static const uint8_t Font_Unifont_Bold_16_glyph_115[] = { 7, 8, 8, 0, 8, 125, 143, 27, 129, 216, 241, 190, 0 };
/* t */ static const uint8_t Font_Unifont_Bold_16_glyph_116[] = { 7, 10, 8, 0, 10, 48, 96, 199, 243, 6, 12, 24, 48, 60 };
/* u */ static const uint8_t Font_Unifont_Bold_16_glyph_117[] = { 7, 8, 8, 0, 8, 199, 143, 30, 60, 120, 243, 187, 0 };
/* v */ static const uint8_t Font_Unifont_Bold_16_glyph_118[] = { 7, 8, 8, 0, 8, 199, 143, 26, 38, 205, 142, 28, 0 };
/* w */ static const uint8_t Font_Unifont_Bold_16_glyph_119[] = { 7, 8, 8, 0, 8, 199, 175, 94, 189, 122, 245, 182, 0 };
/* x */ static const uint8_t Font_Unifont_Bold_16_glyph_120[] = { 7, 8, 8, 0, 8, 199, 141, 177, 195, 141, 177, 227, 0 };
/* y */ static const uint8_t Font_Unifont_Bold_16_glyph_121[] = { 7, 10, 8, 0, 8, 199, 143, 30, 60, 109, 205, 131, 6, 248 };
/* z */ static const uint8_t Font_Unifont_Bold_16_glyph_122[] = { 7, 8, 8, 0, 8, 254, 12, 56, 227, 142, 56, 127, 0 };
/* { */ static const uint8_t Font_Unifont_Bold_16_glyph_123[] = { 5, 13, 7, 1, 11, 59, 24, 99, 51, 12, 49, 152, 195, 128 };
/* | */ static const uint8_t Font_Unifont_Bold_16_glyph_124[] = { 2, 14, 7, 2, 12, 255, 255, 255, 240 };
/* } */ static const uint8_t Font_Unifont_Bold_16_glyph_125[] = { 5, 13, 7, 0, 11, 225, 140, 198, 24, 102, 99, 12, 110, 0 };
/* ~ */ static const uint8_t Font_Unifont_Bold_16_glyph_126[] = { 7, 3, 8, 0, 11, 99, 118, 48 };
const uint8_t Font_Unifont_Bold_16_glyph_nonprintable[] = { 7, 10, 8, 0, 10, 130, 112, 231, 207, 60, 249, 255, 231, 207 };
const uint8_t * const Font_Unifont_Bold_16[126 + 1 - 32] = {
Font_Unifont_Bold_16_glyph_32,
Font_Unifont_Bold_16_glyph_33,
Font_Unifont_Bold_16_glyph_34,
Font_Unifont_Bold_16_glyph_35,
Font_Unifont_Bold_16_glyph_36,
Font_Unifont_Bold_16_glyph_37,
Font_Unifont_Bold_16_glyph_38,
Font_Unifont_Bold_16_glyph_39,
Font_Unifont_Bold_16_glyph_40,
Font_Unifont_Bold_16_glyph_41,
Font_Unifont_Bold_16_glyph_42,
Font_Unifont_Bold_16_glyph_43,
Font_Unifont_Bold_16_glyph_44,
Font_Unifont_Bold_16_glyph_45,
Font_Unifont_Bold_16_glyph_46,
Font_Unifont_Bold_16_glyph_47,
Font_Unifont_Bold_16_glyph_48,
Font_Unifont_Bold_16_glyph_49,
Font_Unifont_Bold_16_glyph_50,
Font_Unifont_Bold_16_glyph_51,
Font_Unifont_Bold_16_glyph_52,
Font_Unifont_Bold_16_glyph_53,
Font_Unifont_Bold_16_glyph_54,
Font_Unifont_Bold_16_glyph_55,
Font_Unifont_Bold_16_glyph_56,
Font_Unifont_Bold_16_glyph_57,
Font_Unifont_Bold_16_glyph_58,
Font_Unifont_Bold_16_glyph_59,
Font_Unifont_Bold_16_glyph_60,
Font_Unifont_Bold_16_glyph_61,
Font_Unifont_Bold_16_glyph_62,
Font_Unifont_Bold_16_glyph_63,
Font_Unifont_Bold_16_glyph_64,
Font_Unifont_Bold_16_glyph_65,
Font_Unifont_Bold_16_glyph_66,
Font_Unifont_Bold_16_glyph_67,
Font_Unifont_Bold_16_glyph_68,
Font_Unifont_Bold_16_glyph_69,
Font_Unifont_Bold_16_glyph_70,
Font_Unifont_Bold_16_glyph_71,
Font_Unifont_Bold_16_glyph_72,
Font_Unifont_Bold_16_glyph_73,
Font_Unifont_Bold_16_glyph_74,
Font_Unifont_Bold_16_glyph_75,
Font_Unifont_Bold_16_glyph_76,
Font_Unifont_Bold_16_glyph_77,
Font_Unifont_Bold_16_glyph_78,
Font_Unifont_Bold_16_glyph_79,
Font_Unifont_Bold_16_glyph_80,
Font_Unifont_Bold_16_glyph_81,
Font_Unifont_Bold_16_glyph_82,
Font_Unifont_Bold_16_glyph_83,
Font_Unifont_Bold_16_glyph_84,
Font_Unifont_Bold_16_glyph_85,
Font_Unifont_Bold_16_glyph_86,
Font_Unifont_Bold_16_glyph_87,
Font_Unifont_Bold_16_glyph_88,
Font_Unifont_Bold_16_glyph_89,
Font_Unifont_Bold_16_glyph_90,
Font_Unifont_Bold_16_glyph_91,
Font_Unifont_Bold_16_glyph_92,
Font_Unifont_Bold_16_glyph_93,
Font_Unifont_Bold_16_glyph_94,
Font_Unifont_Bold_16_glyph_95,
Font_Unifont_Bold_16_glyph_96,
Font_Unifont_Bold_16_glyph_97,
Font_Unifont_Bold_16_glyph_98,
Font_Unifont_Bold_16_glyph_99,
Font_Unifont_Bold_16_glyph_100,
Font_Unifont_Bold_16_glyph_101,
Font_Unifont_Bold_16_glyph_102,
Font_Unifont_Bold_16_glyph_103,
Font_Unifont_Bold_16_glyph_104,
Font_Unifont_Bold_16_glyph_105,
Font_Unifont_Bold_16_glyph_106,
Font_Unifont_Bold_16_glyph_107,
Font_Unifont_Bold_16_glyph_108,
Font_Unifont_Bold_16_glyph_109,
Font_Unifont_Bold_16_glyph_110,
Font_Unifont_Bold_16_glyph_111,
Font_Unifont_Bold_16_glyph_112,
Font_Unifont_Bold_16_glyph_113,
Font_Unifont_Bold_16_glyph_114,
Font_Unifont_Bold_16_glyph_115,
Font_Unifont_Bold_16_glyph_116,
Font_Unifont_Bold_16_glyph_117,
Font_Unifont_Bold_16_glyph_118,
Font_Unifont_Bold_16_glyph_119,
Font_Unifont_Bold_16_glyph_120,
Font_Unifont_Bold_16_glyph_121,
Font_Unifont_Bold_16_glyph_122,
Font_Unifont_Bold_16_glyph_123,
Font_Unifont_Bold_16_glyph_124,
Font_Unifont_Bold_16_glyph_125,
Font_Unifont_Bold_16_glyph_126,
};

View File

@ -0,0 +1,7 @@
#include <stdint.h>
#if TREZOR_FONT_BPP != 1
#error Wrong TREZOR_FONT_BPP (expected 1)
#endif
extern const uint8_t* const Font_Unifont_Bold_16[126 + 1 - 32];
extern const uint8_t Font_Unifont_Bold_16_glyph_nonprintable[];

View File

@ -0,0 +1,203 @@
#include <stdint.h>
// clang-format off
// - the first two bytes are width and height of the glyph
// - the third, fourth and fifth bytes are advance, bearingX and bearingY of the horizontal metrics of the glyph
// - the rest is packed 1-bit glyph data
/* */ static const uint8_t Font_Unifont_Regular_16_glyph_32[] = { 0, 0, 8, 0, 0 };
/* ! */ static const uint8_t Font_Unifont_Regular_16_glyph_33[] = { 1, 10, 7, 3, 10, 254, 192 };
/* " */ static const uint8_t Font_Unifont_Regular_16_glyph_34[] = { 5, 4, 7, 1, 12, 140, 99, 16 };
/* # */ static const uint8_t Font_Unifont_Regular_16_glyph_35[] = { 6, 10, 7, 0, 10, 36, 146, 127, 73, 47, 228, 146, 64 };
/* $ */ static const uint8_t Font_Unifont_Regular_16_glyph_36[] = { 7, 10, 7, 0, 10, 16, 250, 76, 135, 3, 132, 201, 124, 32 };
/* % */ static const uint8_t Font_Unifont_Regular_16_glyph_37[] = { 7, 10, 7, 0, 10, 99, 42, 83, 65, 2, 11, 41, 83, 24 };
/* & */ static const uint8_t Font_Unifont_Regular_16_glyph_38[] = { 7, 10, 7, 0, 10, 56, 137, 17, 67, 10, 98, 194, 140, 228 };
/* ' */ static const uint8_t Font_Unifont_Regular_16_glyph_39[] = { 1, 4, 7, 3, 12, 240 };
/* ( */ static const uint8_t Font_Unifont_Regular_16_glyph_40[] = { 3, 12, 7, 2, 11, 41, 73, 36, 137, 16 };
/* ) */ static const uint8_t Font_Unifont_Regular_16_glyph_41[] = { 3, 12, 7, 1, 11, 137, 18, 73, 41, 64 };
/* * */ static const uint8_t Font_Unifont_Regular_16_glyph_42[] = { 7, 7, 7, 0, 8, 17, 37, 81, 197, 82, 68, 0 };
/* + */ static const uint8_t Font_Unifont_Regular_16_glyph_43[] = { 7, 7, 7, 0, 8, 16, 32, 71, 241, 2, 4, 0 };
/* , */ static const uint8_t Font_Unifont_Regular_16_glyph_44[] = { 2, 4, 7, 2, 2, 214, 0 };
/* - */ static const uint8_t Font_Unifont_Regular_16_glyph_45[] = { 4, 1, 7, 1, 5, 240 };
/* . */ static const uint8_t Font_Unifont_Regular_16_glyph_46[] = { 2, 2, 7, 2, 2, 240 };
/* / */ static const uint8_t Font_Unifont_Regular_16_glyph_47[] = { 6, 10, 7, 0, 10, 4, 16, 132, 16, 130, 16, 130, 0 };
/* 0 */ static const uint8_t Font_Unifont_Regular_16_glyph_48[] = { 6, 10, 7, 0, 10, 49, 40, 99, 150, 156, 97, 72, 192 };
/* 1 */ static const uint8_t Font_Unifont_Regular_16_glyph_49[] = { 5, 10, 7, 1, 10, 35, 40, 66, 16, 132, 39, 192 };
/* 2 */ static const uint8_t Font_Unifont_Regular_16_glyph_50[] = { 6, 10, 7, 0, 10, 122, 24, 65, 24, 132, 32, 131, 240 };
/* 3 */ static const uint8_t Font_Unifont_Regular_16_glyph_51[] = { 6, 10, 7, 0, 10, 122, 24, 65, 56, 16, 97, 133, 224 };
/* 4 */ static const uint8_t Font_Unifont_Regular_16_glyph_52[] = { 6, 10, 7, 0, 10, 8, 98, 146, 138, 47, 194, 8, 32 };
/* 5 */ static const uint8_t Font_Unifont_Regular_16_glyph_53[] = { 6, 10, 7, 0, 10, 254, 8, 32, 248, 16, 65, 133, 224 };
/* 6 */ static const uint8_t Font_Unifont_Regular_16_glyph_54[] = { 6, 10, 7, 0, 10, 57, 8, 32, 250, 24, 97, 133, 224 };
/* 7 */ static const uint8_t Font_Unifont_Regular_16_glyph_55[] = { 6, 10, 7, 0, 10, 252, 16, 66, 8, 33, 4, 16, 64 };
/* 8 */ static const uint8_t Font_Unifont_Regular_16_glyph_56[] = { 6, 10, 7, 0, 10, 122, 24, 97, 122, 24, 97, 133, 224 };
/* 9 */ static const uint8_t Font_Unifont_Regular_16_glyph_57[] = { 6, 10, 7, 0, 10, 122, 24, 97, 124, 16, 65, 9, 192 };
/* : */ static const uint8_t Font_Unifont_Regular_16_glyph_58[] = { 2, 7, 7, 2, 8, 240, 60 };
/* ; */ static const uint8_t Font_Unifont_Regular_16_glyph_59[] = { 2, 9, 7, 2, 8, 240, 53, 128 };
/* < */ static const uint8_t Font_Unifont_Regular_16_glyph_60[] = { 5, 9, 7, 1, 9, 8, 136, 136, 32, 130, 8 };
/* = */ static const uint8_t Font_Unifont_Regular_16_glyph_61[] = { 6, 5, 7, 0, 7, 252, 0, 0, 252 };
/* > */ static const uint8_t Font_Unifont_Regular_16_glyph_62[] = { 5, 9, 7, 0, 9, 130, 8, 32, 136, 136, 128 };
/* ? */ static const uint8_t Font_Unifont_Regular_16_glyph_63[] = { 6, 10, 7, 0, 10, 122, 24, 65, 8, 65, 0, 16, 64 };
/* @ */ static const uint8_t Font_Unifont_Regular_16_glyph_64[] = { 6, 10, 7, 0, 10, 57, 25, 107, 166, 154, 103, 64, 240 };
/* A */ static const uint8_t Font_Unifont_Regular_16_glyph_65[] = { 6, 10, 7, 0, 10, 49, 36, 161, 135, 248, 97, 134, 16 };
/* B */ static const uint8_t Font_Unifont_Regular_16_glyph_66[] = { 6, 10, 7, 0, 10, 250, 24, 97, 250, 24, 97, 135, 224 };
/* C */ static const uint8_t Font_Unifont_Regular_16_glyph_67[] = { 6, 10, 7, 0, 10, 122, 24, 96, 130, 8, 33, 133, 224 };
/* D */ static const uint8_t Font_Unifont_Regular_16_glyph_68[] = { 6, 10, 7, 0, 10, 242, 40, 97, 134, 24, 97, 139, 192 };
/* E */ static const uint8_t Font_Unifont_Regular_16_glyph_69[] = { 6, 10, 7, 0, 10, 254, 8, 32, 250, 8, 32, 131, 240 };
/* F */ static const uint8_t Font_Unifont_Regular_16_glyph_70[] = { 6, 10, 7, 0, 10, 254, 8, 32, 250, 8, 32, 130, 0 };
/* G */ static const uint8_t Font_Unifont_Regular_16_glyph_71[] = { 6, 10, 7, 0, 10, 122, 24, 96, 130, 120, 97, 141, 208 };
/* H */ static const uint8_t Font_Unifont_Regular_16_glyph_72[] = { 6, 10, 7, 0, 10, 134, 24, 97, 254, 24, 97, 134, 16 };
/* I */ static const uint8_t Font_Unifont_Regular_16_glyph_73[] = { 5, 10, 7, 1, 10, 249, 8, 66, 16, 132, 39, 192 };
/* J */ static const uint8_t Font_Unifont_Regular_16_glyph_74[] = { 7, 10, 7, 0, 10, 62, 16, 32, 64, 129, 2, 68, 136, 224 };
/* K */ static const uint8_t Font_Unifont_Regular_16_glyph_75[] = { 6, 10, 7, 0, 10, 134, 41, 40, 195, 10, 36, 138, 16 };
/* L */ static const uint8_t Font_Unifont_Regular_16_glyph_76[] = { 6, 10, 7, 0, 10, 130, 8, 32, 130, 8, 32, 131, 240 };
/* M */ static const uint8_t Font_Unifont_Regular_16_glyph_77[] = { 6, 10, 7, 0, 10, 134, 28, 243, 182, 216, 97, 134, 16 };
/* N */ static const uint8_t Font_Unifont_Regular_16_glyph_78[] = { 6, 10, 7, 0, 10, 135, 28, 105, 166, 89, 99, 142, 16 };
/* O */ static const uint8_t Font_Unifont_Regular_16_glyph_79[] = { 6, 10, 7, 0, 10, 122, 24, 97, 134, 24, 97, 133, 224 };
/* P */ static const uint8_t Font_Unifont_Regular_16_glyph_80[] = { 6, 10, 7, 0, 10, 250, 24, 97, 250, 8, 32, 130, 0 };
/* Q */ static const uint8_t Font_Unifont_Regular_16_glyph_81[] = { 7, 11, 7, 0, 10, 121, 10, 20, 40, 80, 161, 90, 204, 240, 24 };
/* R */ static const uint8_t Font_Unifont_Regular_16_glyph_82[] = { 6, 10, 7, 0, 10, 250, 24, 97, 250, 72, 162, 134, 16 };
/* S */ static const uint8_t Font_Unifont_Regular_16_glyph_83[] = { 6, 10, 7, 0, 10, 122, 24, 96, 96, 96, 97, 133, 224 };
/* T */ static const uint8_t Font_Unifont_Regular_16_glyph_84[] = { 7, 10, 7, 0, 10, 254, 32, 64, 129, 2, 4, 8, 16, 32 };
/* U */ static const uint8_t Font_Unifont_Regular_16_glyph_85[] = { 6, 10, 7, 0, 10, 134, 24, 97, 134, 24, 97, 133, 224 };
/* V */ static const uint8_t Font_Unifont_Regular_16_glyph_86[] = { 7, 10, 7, 0, 10, 131, 6, 10, 36, 72, 138, 20, 16, 32 };
/* W */ static const uint8_t Font_Unifont_Regular_16_glyph_87[] = { 6, 10, 7, 0, 10, 134, 24, 97, 182, 220, 243, 134, 16 };
/* X */ static const uint8_t Font_Unifont_Regular_16_glyph_88[] = { 6, 10, 7, 0, 10, 134, 20, 146, 48, 196, 146, 134, 16 };
/* Y */ static const uint8_t Font_Unifont_Regular_16_glyph_89[] = { 7, 10, 7, 0, 10, 131, 5, 18, 34, 130, 4, 8, 16, 32 };
/* Z */ static const uint8_t Font_Unifont_Regular_16_glyph_90[] = { 6, 10, 7, 0, 10, 252, 16, 66, 16, 132, 32, 131, 240 };
/* [ */ static const uint8_t Font_Unifont_Regular_16_glyph_91[] = { 3, 12, 7, 3, 11, 242, 73, 36, 146, 112 };
/* \ */ static const uint8_t Font_Unifont_Regular_16_glyph_92[] = { 6, 10, 7, 0, 10, 130, 4, 8, 32, 65, 2, 4, 16 };
/* ] */ static const uint8_t Font_Unifont_Regular_16_glyph_93[] = { 3, 12, 7, 0, 11, 228, 146, 73, 36, 240 };
/* ^ */ static const uint8_t Font_Unifont_Regular_16_glyph_94[] = { 6, 3, 7, 0, 12, 49, 40, 64 };
/* _ */ static const uint8_t Font_Unifont_Regular_16_glyph_95[] = { 7, 1, 7, 0, 0, 254 };
/* ` */ static const uint8_t Font_Unifont_Regular_16_glyph_96[] = { 3, 3, 7, 1, 13, 136, 128 };
/* a */ static const uint8_t Font_Unifont_Regular_16_glyph_97[] = { 6, 8, 7, 0, 8, 122, 16, 95, 134, 24, 221, 0 };
/* b */ static const uint8_t Font_Unifont_Regular_16_glyph_98[] = { 6, 11, 7, 0, 11, 130, 8, 46, 198, 24, 97, 135, 27, 128 };
/* c */ static const uint8_t Font_Unifont_Regular_16_glyph_99[] = { 6, 8, 7, 0, 8, 122, 24, 32, 130, 8, 94, 0 };
/* d */ static const uint8_t Font_Unifont_Regular_16_glyph_100[] = { 6, 11, 7, 0, 11, 4, 16, 93, 142, 24, 97, 134, 55, 64 };
/* e */ static const uint8_t Font_Unifont_Regular_16_glyph_101[] = { 6, 8, 7, 0, 8, 122, 24, 127, 130, 8, 94, 0 };
/* f */ static const uint8_t Font_Unifont_Regular_16_glyph_102[] = { 5, 11, 7, 0, 11, 25, 8, 79, 144, 132, 33, 8 };
/* g */ static const uint8_t Font_Unifont_Regular_16_glyph_103[] = { 6, 11, 7, 0, 9, 5, 216, 162, 137, 196, 30, 134, 23, 128 };
/* h */ static const uint8_t Font_Unifont_Regular_16_glyph_104[] = { 6, 11, 7, 0, 11, 130, 8, 46, 198, 24, 97, 134, 24, 64 };
/* i */ static const uint8_t Font_Unifont_Regular_16_glyph_105[] = { 5, 11, 7, 1, 11, 33, 0, 194, 16, 132, 33, 62 };
/* j */ static const uint8_t Font_Unifont_Regular_16_glyph_106[] = { 5, 13, 7, 0, 11, 8, 64, 48, 132, 33, 8, 67, 38, 0 };
/* k */ static const uint8_t Font_Unifont_Regular_16_glyph_107[] = { 6, 11, 7, 0, 11, 130, 8, 34, 146, 140, 40, 146, 40, 64 };
/* l */ static const uint8_t Font_Unifont_Regular_16_glyph_108[] = { 5, 11, 7, 1, 11, 97, 8, 66, 16, 132, 33, 62 };
/* m */ static const uint8_t Font_Unifont_Regular_16_glyph_109[] = { 7, 8, 7, 0, 8, 237, 38, 76, 153, 50, 100, 201, 0 };
/* n */ static const uint8_t Font_Unifont_Regular_16_glyph_110[] = { 6, 8, 7, 0, 8, 187, 24, 97, 134, 24, 97, 0 };
/* o */ static const uint8_t Font_Unifont_Regular_16_glyph_111[] = { 6, 8, 7, 0, 8, 122, 24, 97, 134, 24, 94, 0 };
/* p */ static const uint8_t Font_Unifont_Regular_16_glyph_112[] = { 6, 10, 7, 0, 8, 187, 24, 97, 134, 28, 110, 130, 0 };
/* q */ static const uint8_t Font_Unifont_Regular_16_glyph_113[] = { 6, 10, 7, 0, 8, 118, 56, 97, 134, 24, 221, 4, 16 };
/* r */ static const uint8_t Font_Unifont_Regular_16_glyph_114[] = { 6, 8, 7, 0, 8, 187, 24, 96, 130, 8, 32, 0 };
/* s */ static const uint8_t Font_Unifont_Regular_16_glyph_115[] = { 6, 8, 7, 0, 8, 122, 24, 24, 24, 24, 94, 0 };
/* t */ static const uint8_t Font_Unifont_Regular_16_glyph_116[] = { 5, 10, 7, 0, 10, 33, 9, 242, 16, 132, 32, 192 };
/* u */ static const uint8_t Font_Unifont_Regular_16_glyph_117[] = { 6, 8, 7, 0, 8, 134, 24, 97, 134, 24, 221, 0 };
/* v */ static const uint8_t Font_Unifont_Regular_16_glyph_118[] = { 6, 8, 7, 0, 8, 134, 24, 82, 73, 35, 12, 0 };
/* w */ static const uint8_t Font_Unifont_Regular_16_glyph_119[] = { 7, 8, 7, 0, 8, 131, 38, 76, 153, 50, 100, 182, 0 };
/* x */ static const uint8_t Font_Unifont_Regular_16_glyph_120[] = { 6, 8, 7, 0, 8, 134, 20, 140, 49, 40, 97, 0 };
/* y */ static const uint8_t Font_Unifont_Regular_16_glyph_121[] = { 6, 10, 7, 0, 8, 134, 24, 97, 133, 51, 65, 5, 224 };
/* z */ static const uint8_t Font_Unifont_Regular_16_glyph_122[] = { 6, 8, 7, 0, 8, 252, 16, 132, 33, 8, 63, 0 };
/* { */ static const uint8_t Font_Unifont_Regular_16_glyph_123[] = { 4, 13, 7, 1, 11, 52, 66, 36, 132, 34, 68, 48 };
/* | */ static const uint8_t Font_Unifont_Regular_16_glyph_124[] = { 1, 14, 7, 3, 12, 255, 252 };
/* } */ static const uint8_t Font_Unifont_Regular_16_glyph_125[] = { 4, 13, 7, 1, 11, 194, 36, 66, 18, 68, 34, 192 };
/* ~ */ static const uint8_t Font_Unifont_Regular_16_glyph_126[] = { 7, 3, 7, 0, 11, 99, 38, 48 };
const uint8_t Font_Unifont_Regular_16_glyph_nonprintable[] = { 6, 10, 7, 0, 10, 133, 231, 190, 247, 190, 255, 239, 191 };
const uint8_t * const Font_Unifont_Regular_16[126 + 1 - 32] = {
Font_Unifont_Regular_16_glyph_32,
Font_Unifont_Regular_16_glyph_33,
Font_Unifont_Regular_16_glyph_34,
Font_Unifont_Regular_16_glyph_35,
Font_Unifont_Regular_16_glyph_36,
Font_Unifont_Regular_16_glyph_37,
Font_Unifont_Regular_16_glyph_38,
Font_Unifont_Regular_16_glyph_39,
Font_Unifont_Regular_16_glyph_40,
Font_Unifont_Regular_16_glyph_41,
Font_Unifont_Regular_16_glyph_42,
Font_Unifont_Regular_16_glyph_43,
Font_Unifont_Regular_16_glyph_44,
Font_Unifont_Regular_16_glyph_45,
Font_Unifont_Regular_16_glyph_46,
Font_Unifont_Regular_16_glyph_47,
Font_Unifont_Regular_16_glyph_48,
Font_Unifont_Regular_16_glyph_49,
Font_Unifont_Regular_16_glyph_50,
Font_Unifont_Regular_16_glyph_51,
Font_Unifont_Regular_16_glyph_52,
Font_Unifont_Regular_16_glyph_53,
Font_Unifont_Regular_16_glyph_54,
Font_Unifont_Regular_16_glyph_55,
Font_Unifont_Regular_16_glyph_56,
Font_Unifont_Regular_16_glyph_57,
Font_Unifont_Regular_16_glyph_58,
Font_Unifont_Regular_16_glyph_59,
Font_Unifont_Regular_16_glyph_60,
Font_Unifont_Regular_16_glyph_61,
Font_Unifont_Regular_16_glyph_62,
Font_Unifont_Regular_16_glyph_63,
Font_Unifont_Regular_16_glyph_64,
Font_Unifont_Regular_16_glyph_65,
Font_Unifont_Regular_16_glyph_66,
Font_Unifont_Regular_16_glyph_67,
Font_Unifont_Regular_16_glyph_68,
Font_Unifont_Regular_16_glyph_69,
Font_Unifont_Regular_16_glyph_70,
Font_Unifont_Regular_16_glyph_71,
Font_Unifont_Regular_16_glyph_72,
Font_Unifont_Regular_16_glyph_73,
Font_Unifont_Regular_16_glyph_74,
Font_Unifont_Regular_16_glyph_75,
Font_Unifont_Regular_16_glyph_76,
Font_Unifont_Regular_16_glyph_77,
Font_Unifont_Regular_16_glyph_78,
Font_Unifont_Regular_16_glyph_79,
Font_Unifont_Regular_16_glyph_80,
Font_Unifont_Regular_16_glyph_81,
Font_Unifont_Regular_16_glyph_82,
Font_Unifont_Regular_16_glyph_83,
Font_Unifont_Regular_16_glyph_84,
Font_Unifont_Regular_16_glyph_85,
Font_Unifont_Regular_16_glyph_86,
Font_Unifont_Regular_16_glyph_87,
Font_Unifont_Regular_16_glyph_88,
Font_Unifont_Regular_16_glyph_89,
Font_Unifont_Regular_16_glyph_90,
Font_Unifont_Regular_16_glyph_91,
Font_Unifont_Regular_16_glyph_92,
Font_Unifont_Regular_16_glyph_93,
Font_Unifont_Regular_16_glyph_94,
Font_Unifont_Regular_16_glyph_95,
Font_Unifont_Regular_16_glyph_96,
Font_Unifont_Regular_16_glyph_97,
Font_Unifont_Regular_16_glyph_98,
Font_Unifont_Regular_16_glyph_99,
Font_Unifont_Regular_16_glyph_100,
Font_Unifont_Regular_16_glyph_101,
Font_Unifont_Regular_16_glyph_102,
Font_Unifont_Regular_16_glyph_103,
Font_Unifont_Regular_16_glyph_104,
Font_Unifont_Regular_16_glyph_105,
Font_Unifont_Regular_16_glyph_106,
Font_Unifont_Regular_16_glyph_107,
Font_Unifont_Regular_16_glyph_108,
Font_Unifont_Regular_16_glyph_109,
Font_Unifont_Regular_16_glyph_110,
Font_Unifont_Regular_16_glyph_111,
Font_Unifont_Regular_16_glyph_112,
Font_Unifont_Regular_16_glyph_113,
Font_Unifont_Regular_16_glyph_114,
Font_Unifont_Regular_16_glyph_115,
Font_Unifont_Regular_16_glyph_116,
Font_Unifont_Regular_16_glyph_117,
Font_Unifont_Regular_16_glyph_118,
Font_Unifont_Regular_16_glyph_119,
Font_Unifont_Regular_16_glyph_120,
Font_Unifont_Regular_16_glyph_121,
Font_Unifont_Regular_16_glyph_122,
Font_Unifont_Regular_16_glyph_123,
Font_Unifont_Regular_16_glyph_124,
Font_Unifont_Regular_16_glyph_125,
Font_Unifont_Regular_16_glyph_126,
};

View File

@ -0,0 +1,7 @@
#include <stdint.h>
#if TREZOR_FONT_BPP != 1
#error Wrong TREZOR_FONT_BPP (expected 1)
#endif
extern const uint8_t* const Font_Unifont_Regular_16[126 + 1 - 32];
extern const uint8_t Font_Unifont_Regular_16_glyph_nonprintable[];

View File

@ -6,7 +6,7 @@
// - the third, fourth and fifth bytes are advance, bearingX and bearingY of the horizontal metrics of the glyph
// - the rest is packed 1-bit glyph data
/* */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_32[] = { 0, 0, 8, 0, 0 };
/* */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_32[] = { 0, 0, 7, 0, 0 }; // hand-changed to 7 to have 9px space between words
/* ! */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_33[] = { 1, 7, 7, 2, 7, 250 };
/* " */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_34[] = { 3, 3, 7, 1, 7, 182, 128 };
/* # */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_35[] = { 6, 6, 7, 0, 6, 75, 244, 146, 253, 32 };

View File

@ -16,6 +16,8 @@ static void _librust_qstrs(void) {
MP_QSTR_CONFIRMED;
MP_QSTR_CANCELLED;
MP_QSTR_INFO;
MP_QSTR_checked_index;
MP_QSTR_choices;
MP_QSTR_confirm_action;
MP_QSTR_confirm_blob;
MP_QSTR_confirm_properties;
@ -23,38 +25,50 @@ static void _librust_qstrs(void) {
MP_QSTR_confirm_joint_total;
MP_QSTR_confirm_modify_fee;
MP_QSTR_confirm_modify_output;
MP_QSTR_confirm_output;
MP_QSTR_confirm_output_r;
MP_QSTR_confirm_payment_request;
MP_QSTR_confirm_reset_device;
MP_QSTR_confirm_recovery;
MP_QSTR_confirm_text;
MP_QSTR_confirm_total;
MP_QSTR_confirm_total_r;
MP_QSTR_confirm_value;
MP_QSTR_confirm_with_info;
MP_QSTR_confirm_recovery;
MP_QSTR_show_checklist;
MP_QSTR_show_error;
MP_QSTR_show_qr;
MP_QSTR_show_success;
MP_QSTR_show_warning;
MP_QSTR_show_info;
MP_QSTR_show_simple;
MP_QSTR_request_number;
MP_QSTR_request_pin;
MP_QSTR_request_passphrase;
MP_QSTR_confirm_word;
MP_QSTR_pin_confirm_action;
MP_QSTR_request_bip39;
MP_QSTR_request_number;
MP_QSTR_request_passphrase;
MP_QSTR_request_pin;
MP_QSTR_request_slip39;
MP_QSTR_select_word;
MP_QSTR_select_word_count;
MP_QSTR_share_words;
MP_QSTR_show_checklist;
MP_QSTR_show_error;
MP_QSTR_show_group_share_success;
MP_QSTR_show_info;
MP_QSTR_show_qr;
MP_QSTR_show_remaining_shares;
MP_QSTR_show_success;
MP_QSTR_show_simple;
MP_QSTR_show_warning;
MP_QSTR_show_share_words;
MP_QSTR_request_word_count;
MP_QSTR_request_word_bip39;
MP_QSTR_tutorial;
MP_QSTR_attach_timer_fn;
MP_QSTR_touch_event;
MP_QSTR_button_event;
MP_QSTR_timer;
MP_QSTR_paint;
MP_QSTR_request_complete_repaint;
MP_QSTR_trace;
MP_QSTR_bounds;
MP_QSTR_button_event;
MP_QSTR_page_count;
MP_QSTR_paint;
MP_QSTR_place;
MP_QSTR_request_complete_repaint;
MP_QSTR_touch_event;
MP_QSTR_timer;
MP_QSTR_trace;
MP_QSTR_title;
MP_QSTR_subtitle;
@ -90,4 +104,9 @@ static void _librust_qstrs(void) {
MP_QSTR_active;
MP_QSTR_info_button;
MP_QSTR_time_ms;
MP_QSTR_fee_amount;
MP_QSTR_fee_rate_amount;
MP_QSTR_total_label;
MP_QSTR_fee_label;
MP_QSTR_truncated_address;
}

View File

@ -16,6 +16,7 @@ use super::ffi;
///
/// Given the above assumptions about MicroPython strings, working with
/// StrBuffers in Rust is safe.
#[derive(Debug, Clone)]
pub struct StrBuffer {
ptr: *const u8,
len: usize,

View File

@ -1,3 +1,8 @@
use heapless::String;
#[cfg(feature = "model_tr")]
use crate::ui::model_tr::component::ButtonPos;
/// Visitor passed into `Trace` types.
pub trait Tracer {
fn int(&mut self, i: i64);
@ -6,12 +11,37 @@ pub trait Tracer {
fn symbol(&mut self, name: &str);
fn open(&mut self, name: &str);
fn field(&mut self, name: &str, value: &dyn Trace);
fn title(&mut self, title: &str);
fn button(&mut self, button: &str);
fn content_flag(&mut self);
fn kw_pair(&mut self, key: &str, value: &str);
fn close(&mut self);
}
// Identifiers for tagging various parts of the Trace
// message - so that things like title or the main screen
// content can be read in debug mode by micropython.
pub const TITLE_TAG: &str = " **TITLE** ";
pub const BTN_TAG: &str = " **BTN** ";
pub const CONTENT_TAG: &str = " **CONTENT** ";
// For when the button is not used
pub const EMPTY_BTN: &str = "---";
/// Value that can describe own structure and data using the `Tracer` interface.
pub trait Trace {
fn trace(&self, d: &mut dyn Tracer);
fn trace(&self, t: &mut dyn Tracer);
/// Describes what happens when a certain button is triggered.
#[cfg(feature = "model_tr")]
fn get_btn_action(&self, _pos: ButtonPos) -> String<25> {
"Default".into()
}
/// Report actions for all three buttons in easy-to-parse format.
#[cfg(feature = "model_tr")]
fn report_btn_actions(&self, t: &mut dyn Tracer) {
t.kw_pair("left_action", &self.get_btn_action(ButtonPos::Left));
t.kw_pair("middle_action", &self.get_btn_action(ButtonPos::Middle));
t.kw_pair("right_action", &self.get_btn_action(ButtonPos::Right));
}
}
impl Trace for &[u8] {
@ -26,6 +56,12 @@ impl<const N: usize> Trace for &[u8; N] {
}
}
impl<const N: usize> Trace for String<N> {
fn trace(&self, t: &mut dyn Tracer) {
t.string(&self[..])
}
}
impl Trace for &str {
fn trace(&self, t: &mut dyn Tracer) {
t.string(self);
@ -68,24 +104,54 @@ mod tests {
}
fn symbol(&mut self, name: &str) {
self.extend(name.as_bytes())
self.string("<");
self.string(name);
self.string(">");
}
fn open(&mut self, name: &str) {
self.extend(b"<");
self.extend(name.as_bytes());
self.extend(b" ");
self.string("<");
self.string(name);
self.string(" ");
}
fn field(&mut self, name: &str, value: &dyn Trace) {
self.extend(name.as_bytes());
self.extend(b":");
self.string(name);
self.string(":");
value.trace(self);
self.extend(b" ");
self.string(" ");
}
/// Mark the string as a title/header.
fn title(&mut self, title: &str) {
self.string(TITLE_TAG);
self.string(title);
self.string(TITLE_TAG);
}
/// Mark the string as a button content.
fn button(&mut self, button: &str) {
self.string(BTN_TAG);
self.string(button);
self.string(BTN_TAG);
}
// Mark the following as content visible on the screen,
// until it is called next time.
fn content_flag(&mut self) {
self.string(CONTENT_TAG);
}
/// Key-value pair for easy parsing
fn kw_pair(&mut self, key: &str, value: &str) {
self.string(key);
self.string("::");
self.string(value);
self.string(","); // mostly for human readability
}
fn close(&mut self) {
self.extend(b">")
self.string(">")
}
}
}

View File

@ -44,6 +44,25 @@ pub fn word_completion_mask(prefix: &str) -> u32 {
unsafe { ffi::mnemonic_word_completion_mask(prefix.as_ptr() as _, prefix.len() as _) }
}
/// Returns all possible letters that form a valid word together with some
/// prefix.
pub fn get_available_letters(prefix: &str) -> impl Iterator<Item = char> {
const CHARS: [char; 26] = [
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r',
's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
];
let mask = word_completion_mask(prefix);
CHARS
.iter()
.filter(move |ch| bitmask_from_char(ch) & mask != 0)
.copied()
}
fn bitmask_from_char(ch: &char) -> u32 {
1 << (*ch as u8 - b'a')
}
pub struct Wordlist(&'static [*const cty::c_char]);
impl Wordlist {
@ -190,4 +209,35 @@ mod tests {
.collect::<Vec<_>>();
assert_eq!(result, expected_result);
}
#[test]
fn test_word_completion_mask() {
let result = word_completion_mask("ab");
assert_eq!(result, 0b101000100100100000001);
let result = word_completion_mask("zoo");
assert_eq!(result, 0b0);
}
#[test]
fn test_get_available_letters() {
let expected_result = vec!['a', 'i', 'l', 'o', 's', 'u'];
let result = get_available_letters("ab").collect::<Vec<_>>();
assert_eq!(result, expected_result);
let expected_result = vec!['a', 'e', 'i', 'o', 'u'];
let result = get_available_letters("str").collect::<Vec<_>>();
assert_eq!(result, expected_result);
let result = get_available_letters("zoo").collect::<Vec<_>>();
assert_eq!(result.len(), 0);
}
#[test]
fn test_bitmask_from_char() {
assert_eq!(bitmask_from_char(&'a'), 0b1);
assert_eq!(bitmask_from_char(&'b'), 0b10);
assert_eq!(bitmask_from_char(&'c'), 0b100);
assert_eq!(bitmask_from_char(&'m'), 0b1000000000000);
assert_eq!(bitmask_from_char(&'z'), 0b10000000000000000000000000);
}
}

View File

@ -9,3 +9,22 @@ pub fn shuffle<T>(slice: &mut [T]) {
slice.swap(i, j);
}
}
pub fn uniform_between(min: u32, max: u32) -> u32 {
assert!(max > min);
uniform(max - min + 1) + min
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn uniform_between_test() {
for _ in 0..10 {
assert!((10..=11).contains(&uniform_between(10, 11)));
assert!((10..=12).contains(&uniform_between(10, 12)));
assert!((256..=512).contains(&uniform_between(256, 512)));
}
}
}

View File

@ -83,6 +83,10 @@ impl<T> Child<T> {
self.component
}
pub fn inner_mut(&mut self) -> &mut T {
&mut self.component
}
/// Access inner component mutably, track whether a paint call has been
/// requested, and propagate the flag upwards the component tree.
pub fn mutate<F, U>(&mut self, ctx: &mut EventCtx, component_func: F) -> U

View File

@ -18,6 +18,11 @@ impl Pad {
}
}
pub fn with_clear(mut self) -> Self {
self.clear = true;
self
}
pub fn place(&mut self, area: Rect) {
self.area = area;
}

View File

@ -14,7 +14,9 @@ pub enum PageMsg<T, U> {
}
pub trait Paginate {
/// How many pages of content are there in total?
fn page_count(&mut self) -> usize;
/// Navigate to the given page.
fn change_page(&mut self, active_page: usize);
}
@ -27,6 +29,11 @@ where
let mut page_count = 1; // There's always at least one page.
let mut char_offset = 0;
// Make sure we're starting from the beginning.
self.set_char_offset(char_offset);
// Looping through the content and counting pages
// until we finally fit.
loop {
let fit = self.layout_content(&mut TextNoOp);
match fit {
@ -57,6 +64,8 @@ where
// Make sure we're starting from the beginning.
self.set_char_offset(char_offset);
// Looping through the content until we arrive at
// the wanted page.
while active_page < to_page {
let fit = self.layout_content(&mut TextNoOp);
match fit {

View File

@ -0,0 +1,105 @@
use crate::ui::{component::EventCtx, util::ResultExt};
use heapless::String;
/// Reified editing operations of `TextBox`.
///
/// Note: This does not contain all supported editing operations, only the ones
/// we currently use.
pub enum TextEdit {
ReplaceLast(char),
Append(char),
}
/// Wraps a character buffer of maximum length `L` and provides text editing
/// operations over it. Text ops usually take a `EventCtx` to request a paint
/// pass in case of any state modification.
pub struct TextBox<const L: usize> {
text: String<L>,
}
impl<const L: usize> TextBox<L> {
/// Create a new `TextBox` with content `text`.
pub fn new(text: String<L>) -> Self {
Self { text }
}
/// Create an empty `TextBox`.
pub fn empty() -> Self {
Self::new(String::new())
}
pub fn content(&self) -> &str {
&self.text
}
pub fn len(&self) -> usize {
self.text.len()
}
pub fn is_empty(&self) -> bool {
self.text.is_empty()
}
pub fn is_full(&self) -> bool {
self.text.len() == self.text.capacity()
}
/// Delete the last character of content, if any.
pub fn delete_last(&mut self, ctx: &mut EventCtx) {
let changed = self.text.pop().is_some();
if changed {
ctx.request_paint();
}
}
/// Replaces the last character of the content with `ch`. If the content is
/// empty, `ch` is appended.
pub fn replace_last(&mut self, ctx: &mut EventCtx, ch: char) {
let previous = self.text.pop();
self.text
.push(ch)
.assert_if_debugging_ui("TextBox has zero capacity");
let changed = previous != Some(ch);
if changed {
ctx.request_paint();
}
}
/// Append `ch` at the end of the content.
pub fn append(&mut self, ctx: &mut EventCtx, ch: char) {
self.text.push(ch).assert_if_debugging_ui("TextBox is full");
ctx.request_paint();
}
/// Append `slice` at the end of the content.
pub fn append_slice(&mut self, ctx: &mut EventCtx, slice: &str) {
self.text
.push_str(slice)
.assert_if_debugging_ui("TextBox is full");
ctx.request_paint();
}
/// Replace the textbox content with `text`.
pub fn replace(&mut self, ctx: &mut EventCtx, text: &str) {
if self.text != text {
self.text.clear();
self.text
.push_str(text)
.assert_if_debugging_ui("TextBox is full");
ctx.request_paint();
}
}
/// Clear the textbox content.
pub fn clear(&mut self, ctx: &mut EventCtx) {
self.replace(ctx, "");
}
/// Apply a editing operation to the text buffer.
pub fn apply(&mut self, ctx: &mut EventCtx, edit: TextEdit) {
match edit {
TextEdit::ReplaceLast(char) => self.replace_last(ctx, char),
TextEdit::Append(char) => self.append(ctx, char),
}
}
}

View File

@ -8,23 +8,27 @@ use heapless::LinearMap;
use crate::ui::{
component::{Component, Event, EventCtx, Never},
display::{Color, Font},
geometry::Rect,
geometry::{Rect},
};
use super::layout::{
LayoutFit, LayoutSink, LineBreaking, Op, PageBreaking, TextLayout, TextRenderer, TextStyle,
LayoutFit, LayoutSink, LineBreaking, Op, TextLayout, TextRenderer, TextStyle,
};
pub const MAX_ARGUMENTS: usize = 6;
#[derive(Clone)]
pub struct FormattedText<F, T> {
layout: TextLayout,
fonts: FormattedFonts,
format: F,
args: LinearMap<&'static str, T, MAX_ARGUMENTS>,
/// Keeps track of "cursor" position, so that we can paginate
/// by skipping this amount of characters from the beginning.
char_offset: usize,
}
#[derive(Clone)]
pub struct FormattedFonts {
/// Font used to format `{normal}`.
pub normal: Font,
@ -75,11 +79,8 @@ impl<F, T> FormattedText<F, T> {
self
}
pub fn with_page_breaking(mut self, page_breaking: PageBreaking) -> Self {
self.layout.style.page_breaking = page_breaking;
self
}
/// Equals to changing the page so that we know what
/// content to render next.
pub fn set_char_offset(&mut self, char_offset: usize) {
self.char_offset = char_offset;
}
@ -98,15 +99,26 @@ where
F: AsRef<str>,
T: AsRef<str>,
{
/// Tokenizing `self.format` and turning it into the list of `Op`s
/// which will be sent to `LayoutSink`.
/// It equals to painting the content when `sink` is `TextRenderer`.
pub fn layout_content(&self, sink: &mut dyn LayoutSink) -> LayoutFit {
// Accounting for pagination by skipping the `char_offset` characters from the
// beginning.
let mut cursor = self.layout.initial_cursor();
let mut ops = Op::skip_n_text_bytes(
Tokenizer::new(self.format.as_ref()).flat_map(|arg| match arg {
// Normal text encountered
Token::Literal(literal) => Some(Op::Text(literal)),
// Changing currently used font
Token::Argument("mono") => Some(Op::Font(self.fonts.mono)),
Token::Argument("bold") => Some(Op::Font(self.fonts.bold)),
Token::Argument("normal") => Some(Op::Font(self.fonts.normal)),
Token::Argument("demibold") => Some(Op::Font(self.fonts.demibold)),
// Text with argument
// e.g. `{address}`, .with("address", "abcd...")
// TODO: when arg is not found, we just do not display it,
// shouldn't we trigger some exception?
Token::Argument(argument) => self
.args
.get(argument)
@ -157,7 +169,9 @@ pub mod trace {
T: AsRef<str>,
{
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
d.content_flag();
self.0.layout_content(&mut TraceSink(d));
d.content_flag();
}
}
}

View File

@ -106,18 +106,28 @@ impl TextLayout {
self
}
/// Baseline `Point` where we are starting to draw the text.
pub fn initial_cursor(&self) -> Point {
self.bounds.top_left() + Offset::y(self.style.text_font.text_height() + self.padding_top)
}
/// Trying to fit the content on the current screen.
pub fn fit_text(&self, text: &str) -> LayoutFit {
self.layout_text(text, &mut self.initial_cursor(), &mut TextNoOp)
}
/// Draw as much text as possible on the current screen.
pub fn render_text(&self, text: &str) {
self.layout_text(text, &mut self.initial_cursor(), &mut TextRenderer);
}
/// Y coordinate of the bottom of the available space/bounds
pub fn bottom_y(&self) -> i16 {
(self.bounds.y1 - self.padding_bottom).max(self.bounds.y0)
}
/// Perform some operations defined on `Op` for a list of those `Op`s
/// - e.g. changing the color, changing the font or rendering the text.
pub fn layout_ops<'o>(
mut self,
ops: &mut dyn Iterator<Item = Op<'o>>,
@ -129,12 +139,16 @@ impl TextLayout {
for op in ops {
match op {
// Changing color
Op::Color(color) => {
self.style.text_color = color;
}
// Changing font
Op::Font(font) => {
self.style.text_font = font;
}
// Text - try to fit it on the current page and if it doesn't
// fit, return the appropriate OutOfBounds message
Op::Text(text) => match self.layout_text(text, cursor, sink) {
LayoutFit::Fitting {
processed_chars, ..
@ -161,6 +175,9 @@ impl TextLayout {
}
}
/// Loop through the `text` and try to fit it on the current screen,
/// reporting events to `sink`, which may do something with them (e.g. draw
/// on screen).
pub fn layout_text(
&self,
text: &str,
@ -168,11 +185,10 @@ impl TextLayout {
sink: &mut dyn LayoutSink,
) -> LayoutFit {
let init_cursor = *cursor;
let bottom = (self.bounds.y1 - self.padding_bottom).max(self.bounds.y0);
let mut remaining_text = text;
// Check if bounding box is high enough for at least one line.
if cursor.y > bottom {
if cursor.y > self.bottom_y() {
sink.out_of_bounds();
return LayoutFit::OutOfBounds {
processed_chars: 0,
@ -196,7 +212,12 @@ impl TextLayout {
};
// Report the span at the cursor position.
sink.text(*cursor, self, &remaining_text[..span.length]);
// Not doing it when the span length is 0, as that
// means we encountered a newline/line-break, which we do not draw.
// Line-breaks are reported later.
if span.length > 0 {
sink.text(*cursor, self, &remaining_text[..span.length]);
}
// Continue with the rest of the remaining_text.
remaining_text = &remaining_text[span.length + span.skip_next_chars..];
@ -212,7 +233,7 @@ impl TextLayout {
sink.hyphen(*cursor, self);
}
// Check the amount of vertical space we have left.
if cursor.y + span.advance.y > bottom {
if cursor.y + span.advance.y > self.bottom_y() {
if !remaining_text.is_empty() {
// Append ellipsis to indicate more content is available, but only if we
// haven't already appended a hyphen.
@ -252,6 +273,7 @@ impl TextLayout {
}
}
/// Overall height of the content, including paddings.
fn layout_height(&self, init_cursor: Point, end_cursor: Point) -> i16 {
self.padding_top
+ self.style.text_font.text_height()
@ -270,6 +292,8 @@ impl Dimensions for TextLayout {
}
}
/// Whether we can fit content on the current screen.
/// Knows how many characters got processed and how high the content is.
pub enum LayoutFit {
/// Entire content fits. Vertical size is returned in `height`.
Fitting { processed_chars: usize, height: i16 },
@ -278,6 +302,7 @@ pub enum LayoutFit {
}
impl LayoutFit {
/// How high is the processed/fitted content.
pub fn height(&self) -> i16 {
match self {
LayoutFit::Fitting { height, .. } => *height,
@ -286,19 +311,33 @@ impl LayoutFit {
}
}
// TODO: LayoutSink could support even things like drawing icons
// or making custom x or y offsets from any position
/// Visitor for text segment operations.
/// Defines responses for certain kind of events encountered
/// when processing the content.
pub trait LayoutSink {
/// Text should be processed.
fn text(&mut self, _cursor: Point, _layout: &TextLayout, _text: &str) {}
/// Hyphen at the end of line.
fn hyphen(&mut self, _cursor: Point, _layout: &TextLayout) {}
/// Ellipsis at the end of the page.
fn ellipsis(&mut self, _cursor: Point, _layout: &TextLayout) {}
/// Line break - a newline.
fn line_break(&mut self, _cursor: Point) {}
/// Content cannot fit on the screen.
fn out_of_bounds(&mut self) {}
}
/// `LayoutSink` without any functionality.
/// Used to consume events when counting pages
/// or navigating to a certain page number.
pub struct TextNoOp;
impl LayoutSink for TextNoOp {}
/// `LayoutSink` for rendering the content.
pub struct TextRenderer;
impl LayoutSink for TextRenderer {
@ -339,6 +378,7 @@ pub mod trace {
use super::*;
/// `LayoutSink` for debugging purposes.
pub struct TraceSink<'a>(pub &'a mut dyn crate::trace::Tracer);
impl<'a> LayoutSink for TraceSink<'a> {
@ -360,6 +400,7 @@ pub mod trace {
}
}
/// Operations that can be done on FormattedText.
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum Op<'a> {
/// Render text with current color and font.
@ -371,6 +412,10 @@ pub enum Op<'a> {
}
impl<'a> Op<'a> {
/// Filtering the list of `Op`s to throw away all the content
/// (text, font change or other operations) before a specific byte/character
/// threshold. Used when showing the second, third... paginated page
/// to skip the first one, two... pages.
pub fn skip_n_text_bytes(
ops: impl Iterator<Item = Op<'a>>,
skip_bytes: usize,
@ -392,19 +437,21 @@ impl<'a> Op<'a> {
}
}
/// Carries info about the content that was processed
/// on the current line.
#[derive(Debug, PartialEq, Eq)]
struct Span {
pub struct Span {
/// How many characters from the input text this span is laying out.
length: usize,
pub length: usize,
/// How many chars from the input text should we skip before fitting the
/// next span?
skip_next_chars: usize,
pub skip_next_chars: usize,
/// By how much to offset the cursor after this span. If the vertical offset
/// is bigger than zero, it means we are breaking the line.
advance: Offset,
pub advance: Offset,
/// If we are breaking the line, should we insert a hyphen right after this
/// span to indicate a word-break?
insert_hyphen_before_line_break: bool,
pub insert_hyphen_before_line_break: bool,
}
impl Span {
@ -463,7 +510,7 @@ impl Span {
}
found_any_whitespace = true;
} else if span_width + char_width > max_width {
// Return the last breakpoint.
// Cannot fit on this line. Return the last breakpoint.
return line;
} else {
let have_space_for_break = span_width + char_width + hyphen_width <= max_width;
@ -484,7 +531,7 @@ impl Span {
span_width += char_width;
}
// The whole text is fitting.
// The whole text is fitting on the current line.
Self {
length: text.len(),
advance: Offset::x(span_width),

View File

@ -1,3 +1,4 @@
pub mod common;
pub mod formatted;
mod iter;
pub mod layout;

View File

@ -239,11 +239,13 @@ pub mod trace {
impl<T: ParagraphSource> crate::trace::Trace for Paragraphs<T> {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("Paragraphs");
t.content_flag();
for (layout, content) in Self::visible_content(&self.source, &self.visible, self.offset)
{
layout.layout_text(content, &mut layout.initial_cursor(), &mut TraceSink(t));
t.string("\n");
}
t.content_flag();
t.close();
}
}

View File

@ -0,0 +1,258 @@
//! Including some useful debugging features,
//! like printing of the struct details.
use heapless::String;
use super::{
component::{
pad::Pad,
text::{
common::TextBox,
layout::{Span, TextLayout},
},
},
display::{Color, Font, Icon},
geometry::{Grid, Insets, Offset, Point, Rect},
};
use crate::{micropython::buffer::StrBuffer, time::Duration};
#[cfg(feature = "model_tr")]
use super::model_tr::component::ButtonDetails;
// NOTE: not defining a common trait, like
// Debug {fn print(&self);}, so that the trait does
// not need to be imported when using the
// print() function. It suits the use-case of being quickly
// able to use the print() for debugging and then delete it.
/// TODO: find out how much storage these functions take
/// and probably hide them behind debug feature
impl StrBuffer {
pub fn print(&self) {
println!("StrBuffer:: ", self.as_ref());
}
}
impl Duration {
pub fn print(&self) {
println!("Duration:: ", inttostr!(self.to_millis()));
}
}
impl Point {
pub fn print(&self) {
println!(
"Point:: ",
"x: ",
inttostr!(self.x),
", y: ",
inttostr!(self.y)
);
}
}
impl Rect {
pub fn print(&self) {
print!("Rect:: ");
println!(&self.corners_points());
}
pub fn corners_points(&self) -> String<30> {
build_string!(
30,
"(",
inttostr!(self.x0),
",",
inttostr!(self.y0),
"), (",
inttostr!(self.x1),
",",
inttostr!(self.y1),
")"
)
}
}
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for Rect {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("Rect");
t.string(&self.corners_points());
t.close();
}
}
impl Color {
pub fn print(&self) {
println!(
"Color:: ",
"R: ",
inttostr!(self.r()),
", G: ",
inttostr!(self.g()),
", B: ",
inttostr!(self.b())
);
}
}
impl Font {
pub fn print(&self) {
println!("Font:: ", "text_height: ", inttostr!(self.text_height()));
}
}
#[cfg(feature = "model_tr")]
impl<T: Clone + AsRef<str>> ButtonDetails<T> {
pub fn print(&self) {
let text: String<20> = if let Some(text) = self.text.clone() {
text.as_ref().into()
} else {
"None".into()
};
let icon_text: String<20> = if let Some(icon) = &self.icon {
icon.text.into()
} else {
"None".into()
};
let force_width: String<20> = if let Some(force_width) = self.force_width {
inttostr!(force_width).into()
} else {
"None".into()
};
println!(
"ButtonDetails:: ",
"text: ",
text.as_ref(),
", icon_text: ",
icon_text.as_ref(),
", with_outline: ",
booltostr!(self.with_outline),
", with_arms: ",
booltostr!(self.with_arms),
", force_width: ",
force_width.as_ref()
);
}
}
impl Offset {
pub fn print(&self) {
println!(
"Offset:: ",
"x: ",
inttostr!(self.x),
", y: ",
inttostr!(self.y)
);
}
}
impl Insets {
pub fn print(&self) {
println!(
"Insets:: ",
"top: ",
inttostr!(self.top),
", right: ",
inttostr!(self.right),
", bottom: ",
inttostr!(self.bottom),
", left: ",
inttostr!(self.left)
);
}
}
impl Grid {
pub fn print(&self) {
print!(
"Grid:: ",
"rows: ",
inttostr!(self.rows as i32),
", cols: ",
inttostr!(self.cols as i32),
", spacing: ",
inttostr!(self.spacing as i32)
);
print!(", area: ");
self.area.print();
}
}
impl Icon {
pub fn dimension_str(&self) -> String<10> {
build_string!(
10,
inttostr!(self.width() as i32),
"x",
inttostr!(self.height() as i32)
)
}
pub fn print(&self) {
println!(
"Icon:: ",
"text: ",
self.text,
", width: ",
inttostr!(self.width() as i32),
", height: ",
inttostr!(self.height() as i32)
);
}
}
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for Icon {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("Icon");
t.string(self.text);
t.string(&self.dimension_str());
t.close();
}
}
impl TextLayout {
pub fn print(&self) {
print!(
"TextLayout:: ",
"padding_top: ",
inttostr!(self.padding_top as i32),
", padding_bottom: ",
inttostr!(self.padding_bottom as i32)
);
print!(", bounds: ");
self.bounds.print();
}
}
impl Span {
pub fn print(&self) {
print!(
"Span:: ",
"length: ",
inttostr!(self.length as i32),
", skip_next_chars: ",
inttostr!(self.skip_next_chars as i32),
", insert_hyphen_before_line_break: ",
booltostr!(self.insert_hyphen_before_line_break)
);
print!(", advance: ");
self.advance.print();
}
}
impl Pad {
pub fn print(&self) {
print!("Pad:: ", "area: ");
self.area.print();
}
}
impl<const L: usize> TextBox<L> {
pub fn print(&self) {
println!("TextBox:: ", "content: ", self.content());
}
}

View File

@ -0,0 +1,80 @@
use crate::ui::lerp::Lerp;
#[derive(Copy, Clone, PartialEq, Eq)]
pub struct Color(u16);
impl Color {
pub const fn from_u16(val: u16) -> Self {
Self(val)
}
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
let r = (r as u16 & 0xF8) << 8;
let g = (g as u16 & 0xFC) << 3;
let b = (b as u16 & 0xF8) >> 3;
Self(r | g | b)
}
pub const fn luminance(self) -> u32 {
((self.r() as u32 * 299) / 1000)
+ (self.g() as u32 * 587) / 1000
+ (self.b() as u32 * 114) / 1000
}
pub const fn r(self) -> u8 {
(self.0 >> 8) as u8 & 0xF8
}
pub const fn g(self) -> u8 {
(self.0 >> 3) as u8 & 0xFC
}
pub const fn b(self) -> u8 {
(self.0 << 3) as u8 & 0xF8
}
pub fn to_u16(self) -> u16 {
self.0
}
pub fn hi_byte(self) -> u8 {
(self.to_u16() >> 8) as u8
}
pub fn lo_byte(self) -> u8 {
(self.to_u16() & 0xFF) as u8
}
pub fn negate(self) -> Self {
Self(!self.0)
}
pub const fn white() -> Self {
Self::rgb(255, 255, 255)
}
pub const fn black() -> Self {
Self::rgb(0, 0, 0)
}
}
impl Lerp for Color {
fn lerp(a: Self, b: Self, t: f32) -> Self {
let r = u8::lerp(a.r(), b.r(), t);
let g = u8::lerp(a.g(), b.g(), t);
let b = u8::lerp(a.b(), b.b(), t);
Color::rgb(r, g, b)
}
}
impl From<u16> for Color {
fn from(val: u16) -> Self {
Self(val)
}
}
impl From<Color> for u16 {
fn from(val: Color) -> Self {
val.to_u16()
}
}

View File

@ -0,0 +1,176 @@
use crate::{ui::{constant, geometry::{Point, Offset, Rect}}, trezorhal::display};
use core::slice;
use super::{Color, get_color_table, pixeldata, set_window, get_offset};
pub struct Glyph {
pub width: i16,
pub height: i16,
pub adv: i16,
pub bearing_x: i16,
pub bearing_y: i16,
data: &'static [u8],
}
impl Glyph {
/// Construct a `Glyph` from a raw pointer.
///
/// # Safety
///
/// This function is unsafe because the caller has to guarantee that `data`
/// is pointing to a memory containing a valid glyph data, that is:
/// - contains valid glyph metadata
/// - data has appropriate size
/// - data must have static lifetime
pub unsafe fn load(data: *const u8) -> Self {
unsafe {
let width = *data.offset(0) as i16;
let height = *data.offset(1) as i16;
let data_bits = constant::FONT_BPP * width * height;
let data_bytes = if data_bits % 8 == 0 {
data_bits / 8
} else {
(data_bits / 8) + 1
};
Glyph {
width,
height,
adv: *data.offset(2) as i16,
bearing_x: *data.offset(3) as i16,
bearing_y: *data.offset(4) as i16,
data: slice::from_raw_parts(data.offset(5), data_bytes as usize),
}
}
}
pub fn print(&self, pos: Point, colortable: [Color; 16]) -> i16 {
let bearing = Offset::new(self.bearing_x, -self.bearing_y);
let size = Offset::new(self.width, self.height);
let pos_adj = pos + bearing;
let r = Rect::from_top_left_and_size(pos_adj, size);
let area = r.translate(get_offset());
let window = area.clamp(constant::screen());
set_window(window);
for y in window.y0..window.y1 {
for x in window.x0..window.x1 {
let p = Point::new(x, y);
let r = p - pos_adj;
let c = self.get_pixel_data(r);
pixeldata(colortable[c as usize]);
}
}
self.adv
}
/// Returns 0 (black) or 15 (white).
pub fn unpack_bpp1(&self, a: i16) -> u8 {
let c_data = self.data[(a / 8) as usize];
((c_data >> (7 - (a % 8))) & 0x01) * 15
}
pub fn unpack_bpp2(&self, a: i16) -> u8 {
let c_data = self.data[(a / 4) as usize];
((c_data >> (6 - (a % 4) * 2)) & 0x03) * 5
}
pub fn unpack_bpp4(&self, a: i16) -> u8 {
let c_data = self.data[(a / 2) as usize];
(c_data >> (4 - (a % 2) * 4)) & 0x0F
}
pub fn unpack_bpp8(&self, a: i16) -> u8 {
let c_data = self.data[a as usize];
c_data >> 4
}
pub fn get_pixel_data(&self, p: Offset) -> u8 {
let a = p.x + p.y * self.width;
match constant::FONT_BPP {
1 => self.unpack_bpp1(a),
2 => self.unpack_bpp2(a),
4 => self.unpack_bpp4(a),
8 => self.unpack_bpp8(a),
_ => 0,
}
}
}
/// Font constants. Keep in sync with FONT_ definitions in
/// `extmod/modtrezorui/fonts/fonts.h`.
#[derive(Copy, Clone, PartialEq, Eq)]
#[repr(u8)]
pub enum Font {
NORMAL = 1,
BOLD = 2,
MONO = 3,
DEMIBOLD = 5,
}
impl From<Font> for i32 {
fn from(font: Font) -> i32 {
-(font as i32)
}
}
impl Font {
pub fn text_width(self, text: &str) -> i16 {
display::text_width(text, self.into()) as i16
}
pub fn char_width(self, ch: char) -> i16 {
display::char_width(ch, self.into()) as i16
}
pub fn text_height(self) -> i16 {
display::text_height(self.into()) as i16
}
pub fn line_height(self) -> i16 {
constant::LINE_SPACE + self.text_height()
}
pub fn get_glyph(self, char_byte: u8) -> Option<Glyph> {
let gl_data = display::get_char_glyph(char_byte, self.into());
if gl_data.is_null() {
return None;
}
unsafe { Some(Glyph::load(gl_data)) }
}
pub fn display_text(self, text: &str, baseline: Point, fg_color: Color, bg_color: Color) {
let colortable = get_color_table(fg_color, bg_color);
let mut adv_total = 0;
for c in text.bytes() {
let g = self.get_glyph(c);
if let Some(gly) = g {
let adv = gly.print(baseline + Offset::new(adv_total, 0), colortable);
adv_total += adv;
}
}
}
/// Get the length of the longest suffix from a given `text`
/// that will fit into the area `width` pixels wide.
pub fn longest_suffix(self, width: i16, text: &str) -> usize {
let mut text_width = 0;
for (chars_from_right, c) in text.chars().rev().enumerate() {
let c_width = self.char_width(c);
if text_width + c_width > width {
// Another character cannot be fitted, we're done.
return chars_from_right;
}
text_width += c_width;
}
text.len() // it fits in its entirety
}
}

View File

@ -0,0 +1,97 @@
use crate::{
trezorhal::display::ToifFormat,
ui::geometry::{Offset, Point, Rect},
};
use super::{icon_rect, toif_info_ensure, Color};
/// Storing the icon together with its name
/// Needs to be a tuple-struct, so it can be made `const`
#[derive(Debug, Clone, Copy)]
pub struct IconAndName(&'static [u8], &'static str);
impl IconAndName {
pub const fn new(icon: &'static [u8], name: &'static str) -> Self {
Self(icon, name)
}
}
/// Holding icon data and allowing it to draw itself.
/// Lots of draw methods exist so that we can easily
/// "glue" the icon together with other elements
/// (text, display boundary, etc.) according to their position.
#[derive(Debug, Clone, Copy)]
pub struct Icon {
pub data: &'static [u8],
// Text is useful for debugging purposes.
pub text: &'static str,
// TODO: could include the info about "real" icon dimensions,
// accounting for the TOIF limitations (when we sometimes
// need to have empty row or column) - it could be
// erasing those empty rows/columns when we draw the icon.
}
// TODO: consider merging it together with ToifInfo
impl Icon {
pub fn new(icon_and_name: IconAndName) -> Self {
Icon {
data: icon_and_name.0,
text: icon_and_name.1,
}
}
fn toif_info(&self) -> (Offset, &[u8]) {
toif_info_ensure(self.data, ToifFormat::GrayScaleEH)
}
pub fn width(&self) -> i16 {
self.toif_info().0.x as i16
}
pub fn height(&self) -> i16 {
self.toif_info().0.y as i16
}
pub fn toif_size(&self) -> Offset {
self.toif_info().0
}
pub fn toif_data(&self) -> &[u8] {
self.toif_info().1
}
/// Display icon at a specified Rectangle.
fn draw_icon_rect(&self, r: Rect, fg_color: Color, bg_color: Color) {
icon_rect(r, self.toif_data(), fg_color, bg_color);
}
/// Display the icon with left top baseline Point.
pub fn draw_top_left(&self, baseline: Point, fg_color: Color, bg_color: Color) {
let r = Rect::from_top_left_and_size(baseline, self.toif_size());
self.draw_icon_rect(r, fg_color, bg_color);
}
/// Display the icon with right top baseline Point.
pub fn draw_top_right(&self, baseline: Point, fg_color: Color, bg_color: Color) {
let r = Rect::from_top_right_and_size(baseline, self.toif_size());
self.draw_icon_rect(r, fg_color, bg_color);
}
/// Display the icon with right bottom baseline Point.
pub fn draw_bottom_right(&self, baseline: Point, fg_color: Color, bg_color: Color) {
let r = Rect::from_bottom_right_and_size(baseline, self.toif_size());
self.draw_icon_rect(r, fg_color, bg_color);
}
/// Display the icon with left bottom baseline Point.
pub fn draw_bottom_left(&self, baseline: Point, fg_color: Color, bg_color: Color) {
let r = Rect::from_bottom_left_and_size(baseline, self.toif_size());
self.draw_icon_rect(r, fg_color, bg_color);
}
/// Display the icon around center Point.
pub fn draw_center(&self, center: Point, fg_color: Color, bg_color: Color) {
let r = Rect::from_center_and_size(center, self.toif_size());
self.draw_icon_rect(r, fg_color, bg_color);
}
}

View File

@ -1,5 +1,8 @@
#[cfg(any(feature = "model_tt", feature = "model_tr"))]
pub mod loader;
pub mod icon;
pub mod font;
pub mod color;
use super::{
constant,
@ -24,10 +27,13 @@ use crate::{
},
ui::lerp::Lerp,
};
use core::slice;
// Reexports
#[cfg(any(feature = "model_tt", feature = "model_tr"))]
pub use loader::{loader, loader_indeterminate, LOADER_MAX, LOADER_MIN};
pub use icon::{Icon, IconAndName};
pub use font::{Font, Glyph};
pub use color::Color;
pub fn backlight() -> i32 {
display::backlight(-1)
@ -55,6 +61,7 @@ pub fn fade_backlight(target: i32) {
}
}
/// Fill a whole rectangle with a specific color.
pub fn rect_fill(r: Rect, fg_color: Color) {
display::bar(r.x0, r.y0, r.width(), r.height(), fg_color.into());
}
@ -66,17 +73,22 @@ pub fn rect_stroke(r: Rect, fg_color: Color) {
display::bar(r.x0 + r.width() - 1, r.y0, 1, r.height(), fg_color.into());
}
/// Draw a rectangle with rounded corners.
pub fn rect_fill_rounded(r: Rect, fg_color: Color, bg_color: Color, radius: u8) {
assert!([2, 4, 8, 16].iter().any(|allowed| radius == *allowed));
display::bar_radius(
r.x0,
r.y0,
r.width(),
r.height(),
fg_color.into(),
bg_color.into(),
radius,
);
if radius == 1 {
rect_fill_rounded1(r, fg_color, bg_color);
} else {
assert!([2, 4, 8, 16].iter().any(|allowed| radius == *allowed));
display::bar_radius(
r.x0,
r.y0,
r.width(),
r.height(),
fg_color.into(),
bg_color.into(),
radius,
);
}
}
/// NOTE: Cannot start at odd x-coordinate. In this case icon is shifted 1px
@ -94,9 +106,15 @@ pub fn icon_top_left(top_left: Point, data: &[u8], fg_color: Color, bg_color: Co
);
}
/// Display icon given a center Point.
pub fn icon(center: Point, data: &[u8], fg_color: Color, bg_color: Color) {
let (toif_size, toif_data) = toif_info_ensure(data, ToifFormat::GrayScaleEH);
let r = Rect::from_center_and_size(center, toif_size);
icon_rect(r, toif_data, fg_color, bg_color);
}
/// Display icon at a specified Rectangle, expects already sliced data without header.
pub fn icon_rect(r: Rect, toif_data: &[u8], fg_color: Color, bg_color: Color) {
display::icon(
r.x0,
r.y0,
@ -179,35 +197,61 @@ fn toif_info_ensure(data: &[u8], format: ToifFormat) -> (Offset, &[u8]) {
(size, payload)
}
// Used on T1 only.
/// Filling a rectangle with a rounding of 1 pixel - removing the corners.
pub fn rect_fill_rounded1(r: Rect, fg_color: Color, bg_color: Color) {
display::bar(r.x0, r.y0, r.width(), r.height(), fg_color.into());
let corners = [
r.top_left(),
r.top_right() - Offset::x(1),
r.bottom_right() - Offset::uniform(1),
r.bottom_left() - Offset::y(1),
];
for p in corners.iter() {
display::bar(p.x, p.y, 1, 1, bg_color.into());
rect_fill(r, fg_color);
rect_fill_corners(r, bg_color);
}
/// Creating a rectangular outline with a given radius/rounding.
pub fn rect_outline_rounded(r: Rect, fg_color: Color, bg_color: Color, radius: u8) {
// Painting a bigger rectangle with FG and inner smaller with BG
// to create the outline.
let inner_r = r.shrink(1);
if radius == 1 {
rect_fill_rounded(r, fg_color, bg_color, 1);
rect_fill(inner_r, bg_color);
} else if radius == 2 {
rect_fill_rounded(r, fg_color, bg_color, 2);
rect_fill_rounded(inner_r, bg_color, fg_color, 1);
} else if radius == 4 {
rect_fill_rounded(r, fg_color, bg_color, 4);
rect_fill_rounded(inner_r, bg_color, fg_color, 2);
rect_fill_corners(inner_r, bg_color);
}
}
/// Filling all four corners of a rectangle with a given color.
pub fn rect_fill_corners(r: Rect, fg_color: Color) {
for p in r.corner_points().iter() {
paint_point(p, fg_color);
}
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub struct TextOverlay<'a> {
pub struct TextOverlay<T> {
area: Rect,
text: &'a str,
text: T,
font: Font,
}
impl<'a> TextOverlay<'a> {
pub fn new(text: &'a str, font: Font) -> Self {
impl<T: AsRef<str>> TextOverlay<T> {
pub fn new(text: T, font: Font) -> Self {
let area = Rect::zero();
Self { area, text, font }
}
pub fn set_text(&mut self, text: T) {
self.text = text;
}
pub fn get_text(&self) -> &T {
&self.text
}
// baseline relative to the underlying render area
pub fn place(&mut self, baseline: Point) {
let text_width = self.font.text_width(self.text);
let text_width = self.font.text_width(self.text.as_ref());
let text_height = self.font.text_height();
let text_area_start = baseline + Offset::new(-(text_width / 2), -text_height);
@ -226,7 +270,12 @@ impl<'a> TextOverlay<'a> {
let p_rel = Point::new(p.x - self.area.x0, p.y - self.area.y0);
for g in self.text.bytes().filter_map(|c| self.font.get_glyph(c)) {
for g in self
.text
.as_ref()
.bytes()
.filter_map(|c| self.font.get_glyph(c))
{
let char_area = Rect::new(
Point::new(tot_adv + g.bearing_x, g.height - g.bearing_y),
Point::new(tot_adv + g.bearing_x + g.width, g.bearing_y),
@ -788,9 +837,9 @@ fn rect_rounded2_get_pixel(
/// Optionally draws a text inside the rectangle and adjusts its color to match
/// the fill. The coordinates of the text are specified in the TextOverlay
/// struct.
pub fn bar_with_text_and_fill(
pub fn bar_with_text_and_fill<T: AsRef<str>>(
area: Rect,
overlay: Option<TextOverlay>,
overlay: Option<&TextOverlay<T>>,
fg_color: Color,
bg_color: Color,
fill_from: i16,
@ -833,6 +882,19 @@ pub fn dotted_line(start: Point, width: i16, color: Color) {
}
}
/// Draws a horizontal line of pixels with a given step of pixels.
/// Giving `step=1` draws a full line.
pub fn dotted_line_horizontal(start: Point, width: i16, color: Color, step: usize) {
for x in (start.x..width).step_by(step) {
paint_point(&Point::new(x, start.y), color);
}
}
/// Paints a pixel with a specific color on a given point.
pub fn paint_point(point: &Point, color: Color) {
display::bar(point.x, point.y, 1, 1, color.into());
}
pub fn qrcode(center: Point, data: &str, max_size: u32, case_sensitive: bool) -> Result<(), Error> {
qr::render_qrcode(center.x, center.y, data, max_size, case_sensitive)
}
@ -904,244 +966,3 @@ pub fn get_color_table(fg_color: Color, bg_color: Color) -> [Color; 16] {
table
}
pub struct Glyph {
pub width: i16,
pub height: i16,
pub adv: i16,
pub bearing_x: i16,
pub bearing_y: i16,
data: &'static [u8],
}
impl Glyph {
/// Construct a `Glyph` from a raw pointer.
///
/// # Safety
///
/// This function is unsafe because the caller has to guarantee that `data`
/// is pointing to a memory containing a valid glyph data, that is:
/// - contains valid glyph metadata
/// - data has appropriate size
/// - data must have static lifetime
pub unsafe fn load(data: *const u8) -> Self {
unsafe {
let width = *data.offset(0) as i16;
let height = *data.offset(1) as i16;
let data_bits = constant::FONT_BPP * width * height;
let data_bytes = if data_bits % 8 == 0 {
data_bits / 8
} else {
(data_bits / 8) + 1
};
Glyph {
width,
height,
adv: *data.offset(2) as i16,
bearing_x: *data.offset(3) as i16,
bearing_y: *data.offset(4) as i16,
data: slice::from_raw_parts(data.offset(5), data_bytes as usize),
}
}
}
pub fn print(&self, pos: Point, colortable: [Color; 16]) -> i16 {
let bearing = Offset::new(self.bearing_x, -self.bearing_y);
let size = Offset::new(self.width, self.height);
let pos_adj = pos + bearing;
let r = Rect::from_top_left_and_size(pos_adj, size);
let area = r.translate(get_offset());
let window = area.clamp(constant::screen());
set_window(window);
for y in window.y0..window.y1 {
for x in window.x0..window.x1 {
let p = Point::new(x, y);
let r = p - pos_adj;
let c = self.get_pixel_data(r);
pixeldata(colortable[c as usize]);
}
}
self.adv
}
pub fn unpack_bpp1(&self, a: i16) -> u8 {
let c_data = self.data[(a / 8) as usize];
((c_data >> (7 - (a % 8))) & 0x01) * 15
}
pub fn unpack_bpp2(&self, a: i16) -> u8 {
let c_data = self.data[(a / 4) as usize];
((c_data >> (6 - (a % 4) * 2)) & 0x03) * 5
}
pub fn unpack_bpp4(&self, a: i16) -> u8 {
let c_data = self.data[(a / 2) as usize];
(c_data >> (4 - (a % 2) * 4)) & 0x0F
}
pub fn unpack_bpp8(&self, a: i16) -> u8 {
let c_data = self.data[a as usize];
c_data >> 4
}
pub fn get_pixel_data(&self, p: Offset) -> u8 {
let a = p.x + p.y * self.width;
match constant::FONT_BPP {
1 => self.unpack_bpp1(a),
2 => self.unpack_bpp2(a),
4 => self.unpack_bpp4(a),
8 => self.unpack_bpp8(a),
_ => 0,
}
}
}
/// Font constants. Keep in sync with FONT_ definitions in
/// `extmod/modtrezorui/fonts/fonts.h`.
#[derive(Copy, Clone, PartialEq, Eq)]
#[repr(u8)]
pub enum Font {
NORMAL = 1,
BOLD = 2,
MONO = 3,
DEMIBOLD = 5,
}
impl From<Font> for i32 {
fn from(font: Font) -> i32 {
-(font as i32)
}
}
impl Font {
pub fn text_width(self, text: &str) -> i16 {
display::text_width(text, self.into()) as i16
}
pub fn char_width(self, ch: char) -> i16 {
display::char_width(ch, self.into()) as i16
}
pub fn text_height(self) -> i16 {
display::text_height(self.into()) as i16
}
pub fn line_height(self) -> i16 {
constant::LINE_SPACE + self.text_height()
}
pub fn get_glyph(self, char_byte: u8) -> Option<Glyph> {
let gl_data = display::get_char_glyph(char_byte, self.into());
if gl_data.is_null() {
return None;
}
unsafe { Some(Glyph::load(gl_data)) }
}
pub fn display_text(self, text: &str, baseline: Point, fg_color: Color, bg_color: Color) {
let colortable = get_color_table(fg_color, bg_color);
let mut adv_total = 0;
for c in text.bytes() {
let g = self.get_glyph(c);
if let Some(gly) = g {
let adv = gly.print(baseline + Offset::new(adv_total, 0), colortable);
adv_total += adv;
}
}
}
/// Get the length of the longest suffix from a given `text`
/// that will fit into the area `width` pixels wide.
pub fn longest_suffix(self, width: i16, text: &str) -> usize {
let mut text_width = 0;
for (chars_from_right, c) in text.chars().rev().enumerate() {
let c_width = self.char_width(c);
if text_width + c_width > width {
// Another character cannot be fitted, we're done.
return chars_from_right;
}
text_width += c_width;
}
text.len() // it fits in its entirety
}
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub struct Color(u16);
impl Color {
pub const fn from_u16(val: u16) -> Self {
Self(val)
}
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
let r = (r as u16 & 0xF8) << 8;
let g = (g as u16 & 0xFC) << 3;
let b = (b as u16 & 0xF8) >> 3;
Self(r | g | b)
}
pub const fn luminance(self) -> u32 {
((self.r() as u32 * 299) / 1000)
+ (self.g() as u32 * 587) / 1000
+ (self.b() as u32 * 114) / 1000
}
pub const fn r(self) -> u8 {
(self.0 >> 8) as u8 & 0xF8
}
pub const fn g(self) -> u8 {
(self.0 >> 3) as u8 & 0xFC
}
pub const fn b(self) -> u8 {
(self.0 << 3) as u8 & 0xF8
}
pub fn to_u16(self) -> u16 {
self.0
}
pub fn hi_byte(self) -> u8 {
(self.to_u16() >> 8) as u8
}
pub fn lo_byte(self) -> u8 {
(self.to_u16() & 0xFF) as u8
}
pub fn negate(self) -> Self {
Self(!self.0)
}
}
impl Lerp for Color {
fn lerp(a: Self, b: Self, t: f32) -> Self {
let r = u8::lerp(a.r(), b.r(), t);
let g = u8::lerp(a.g(), b.g(), t);
let b = u8::lerp(a.b(), b.b(), t);
Color::rgb(r, g, b)
}
}
impl From<u16> for Color {
fn from(val: u16) -> Self {
Self(val)
}
}
impl From<Color> for u16 {
fn from(val: Color) -> Self {
val.to_u16()
}
}

View File

@ -2,15 +2,20 @@ use crate::{error, ui::geometry::Point};
use core::convert::TryInto;
#[derive(Copy, Clone, PartialEq, Eq)]
// TODO: delete Both and all its usage
// ButtonPos.hit() should not be used anymore
pub enum PhysicalButton {
Left,
Right,
Both,
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum ButtonEvent {
ButtonPressed(PhysicalButton),
ButtonReleased(PhysicalButton),
HoldStarted,
HoldEnded,
}
impl ButtonEvent {
@ -18,6 +23,7 @@ impl ButtonEvent {
let button = match button {
0 => PhysicalButton::Left,
1 => PhysicalButton::Right,
2 => PhysicalButton::Both,
_ => return Err(error::Error::OutOfRange),
};
let result = match event {

View File

@ -136,7 +136,7 @@ impl From<Point> for Offset {
/// A point in 2D space defined by the the `x` and `y` coordinate. Relative
/// coordinates, vectors, and offsets are represented by the `Offset` type.
#[derive(Copy, Clone, PartialEq, Eq)]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Point {
pub x: i16,
pub y: i16,
@ -233,6 +233,21 @@ impl Rect {
}
}
pub const fn from_top_right_and_size(p0: Point, size: Offset) -> Self {
let top_left = Point::new(p0.x - size.x, p0.y);
Self::from_top_left_and_size(top_left, size)
}
pub const fn from_bottom_left_and_size(p0: Point, size: Offset) -> Self {
let top_left = Point::new(p0.x, p0.y - size.y);
Self::from_top_left_and_size(top_left, size)
}
pub const fn from_bottom_right_and_size(p0: Point, size: Offset) -> Self {
let top_left = Point::new(p0.x - size.x, p0.y - size.y);
Self::from_top_left_and_size(top_left, size)
}
pub const fn from_center_and_size(p: Point, size: Offset) -> Self {
Self {
x0: p.x - size.x / 2,
@ -294,10 +309,20 @@ impl Rect {
self.bottom_left().center(self.bottom_right())
}
pub const fn left_center(&self) -> Point {
self.bottom_left().center(self.top_left())
}
pub const fn right_center(&self) -> Point {
self.bottom_right().center(self.top_right())
}
/// Whether a `Point` is inside the `Rect`.
pub const fn contains(&self, point: Point) -> bool {
point.x >= self.x0 && point.x < self.x1 && point.y >= self.y0 && point.y < self.y1
}
/// Create a bigger `Rect` that contains both `self` and `other`.
pub const fn union(&self, other: Self) -> Self {
Self {
x0: min(self.x0, other.x0),
@ -307,6 +332,8 @@ impl Rect {
}
}
/// Create a smaller `Rect` from the bigger one by moving
/// all the four sides closer to the center.
pub const fn inset(&self, insets: Insets) -> Self {
Self {
x0: self.x0 + insets.left,
@ -316,6 +343,12 @@ impl Rect {
}
}
/// Move all the sides closer to the center by the same distance.
pub const fn shrink(&self, size: i16) -> Self {
self.inset(Insets::uniform(size))
}
/// Leave just the left side of a certain `width`.
pub const fn cut_from_left(&self, width: i16) -> Self {
Self {
x0: self.x0,
@ -325,6 +358,7 @@ impl Rect {
}
}
/// Leave just the right side of a certain `width`.
pub const fn cut_from_right(&self, width: i16) -> Self {
Self {
x0: self.x1 - width,
@ -334,6 +368,27 @@ impl Rect {
}
}
/// Make the `Rect` wider to the left side.
pub const fn extend_left(&self, width: i16) -> Self {
Self {
x0: self.x0 - width,
y0: self.y0,
x1: self.x1,
y1: self.y1,
}
}
/// Make the `Rect` wider to the right side.
pub const fn extend_right(&self, width: i16) -> Self {
Self {
x0: self.x0,
y0: self.y0,
x1: self.x1 + width,
y1: self.y1,
}
}
/// Split `Rect` into top and bottom, given the top one's `height`.
pub const fn split_top(self, height: i16) -> (Self, Self) {
let height = clamp(height, 0, self.height());
@ -348,10 +403,12 @@ impl Rect {
(top, bottom)
}
/// Split `Rect` into top and bottom, given the bottom one's `height`.
pub const fn split_bottom(self, height: i16) -> (Self, Self) {
self.split_top(self.height() - height)
}
/// Split `Rect` into left and right, given the left one's `width`.
pub const fn split_left(self, width: i16) -> (Self, Self) {
let width = clamp(width, 0, self.width());
@ -366,10 +423,21 @@ impl Rect {
(left, right)
}
/// Split `Rect` into left and right, given the right one's `width`.
pub const fn split_right(self, width: i16) -> (Self, Self) {
self.split_left(self.width() - width)
}
/// Split `Rect` into left, center and right, given the center one's
/// `width`. Center element is symmetric, left and right have the same
/// size.
pub const fn split_center(self, width: i16) -> (Self, Self, Self) {
let left_right_width = (self.width() - width) / 2;
let (left, center_right) = self.split_left(left_right_width);
let (center, right) = center_right.split_left(width);
(left, center, right)
}
pub const fn clamp(self, limit: Rect) -> Self {
Self {
x0: max(self.x0, limit.x0),
@ -387,6 +455,7 @@ impl Rect {
}
}
/// Moving `Rect` by the given offset.
pub const fn translate(&self, offset: Offset) -> Self {
Self {
x0: self.x0 + offset.x,
@ -395,6 +464,16 @@ impl Rect {
y1: self.y1 + offset.y,
}
}
/// Get all four corner points.
pub fn corner_points(&self) -> [Point; 4] {
[
self.top_left(),
self.top_right() - Offset::x(1),
self.bottom_right() - Offset::uniform(1),
self.bottom_left() - Offset::y(1),
]
}
}
#[derive(Copy, Clone, PartialEq, Eq)]
@ -509,7 +588,7 @@ impl Grid {
let cell_height = (self.area.height() - spacing_height) / nrows;
// Not every area can be fully covered by equal-sized cells and spaces, there
// might be serveral pixels left unused. We'll distribute them by 1px to
// might be several pixels left unused. We'll distribute them by 1px to
// the leftmost cells.
let leftover_width = (self.area.width() - spacing_width) % ncols;
let leftover_height = (self.area.height() - spacing_height) % nrows;

View File

@ -196,6 +196,14 @@ impl LayoutObj {
unsafe { Gc::as_mut(&mut inner.root) }.obj_paint();
}
/// Place but do not paint.
/// Called before getting debug information about current screen.
fn obj_place(&self) {
let mut inner = self.inner.borrow_mut();
// SAFETY: `inner.root` is unique because of the `inner.borrow_mut()`.
unsafe { Gc::as_mut(&mut inner.root) }.obj_place(constant::screen());
}
/// Run a tracing pass over the component tree. Passed `callback` is called
/// with each piece of tracing information. Panics in case the callback
/// raises an exception.
@ -219,30 +227,51 @@ impl LayoutObj {
}
fn symbol(&mut self, name: &str) {
self.0
.call_with_n_args(&[
"<".try_into().unwrap(),
name.try_into().unwrap(),
">".try_into().unwrap(),
])
.unwrap();
self.string("<");
self.string(name);
self.string(">");
}
fn open(&mut self, name: &str) {
self.0
.call_with_n_args(&["<".try_into().unwrap(), name.try_into().unwrap()])
.unwrap();
self.string("<");
self.string(name);
self.string(" ");
}
fn field(&mut self, name: &str, value: &dyn Trace) {
self.0
.call_with_n_args(&[name.try_into().unwrap(), ": ".try_into().unwrap()])
.unwrap();
self.string(name);
self.string(":");
value.trace(self);
self.string(" ");
}
/// Mark the string as a title/header.
fn title(&mut self, title: &str) {
self.string(crate::trace::TITLE_TAG);
self.string(title);
self.string(crate::trace::TITLE_TAG);
}
/// Mark the string as a button content.
fn button(&mut self, button: &str) {
self.string(crate::trace::BTN_TAG);
self.string(button);
self.string(crate::trace::BTN_TAG);
}
fn content_flag(&mut self) {
self.string(crate::trace::CONTENT_TAG);
}
fn kw_pair(&mut self, key: &str, value: &str) {
self.string(key);
self.string("::");
self.string(value);
self.string(","); // mostly for human readability
}
fn close(&mut self) {
self.0.call_with_n_args(&[">".try_into().unwrap()]).unwrap();
self.string(">")
}
}
@ -283,6 +312,7 @@ impl LayoutObj {
Qstr::MP_QSTR_timer => obj_fn_2!(ui_layout_timer).as_obj(),
Qstr::MP_QSTR_paint => obj_fn_1!(ui_layout_paint).as_obj(),
Qstr::MP_QSTR_request_complete_repaint => obj_fn_1!(ui_layout_request_complete_repaint).as_obj(),
Qstr::MP_QSTR_place => obj_fn_1!(ui_layout_place).as_obj(),
Qstr::MP_QSTR_trace => obj_fn_2!(ui_layout_trace).as_obj(),
Qstr::MP_QSTR_bounds => obj_fn_1!(ui_layout_bounds).as_obj(),
Qstr::MP_QSTR_page_count => obj_fn_1!(ui_layout_page_count).as_obj(),
@ -438,6 +468,15 @@ extern "C" fn ui_layout_request_complete_repaint(this: Obj) -> Obj {
unsafe { util::try_or_raise(block) }
}
extern "C" fn ui_layout_place(this: Obj) -> Obj {
let block = || {
let this: Gc<LayoutObj> = this.try_into()?;
this.obj_place();
Ok(Obj::const_true())
};
unsafe { util::try_or_raise(block) }
}
extern "C" fn ui_layout_page_count(this: Obj) -> Obj {
let block = || {
let this: Gc<LayoutObj> = this.try_into()?;

View File

@ -20,6 +20,16 @@ pub fn iter_into_objs<const N: usize>(iterable: Obj) -> Result<[Obj; N], Error>
}
pub fn iter_into_array<T, const N: usize>(iterable: Obj) -> Result<[T; N], Error>
where
T: TryFrom<Obj, Error = Error>,
{
let err = Error::ValueError(cstr!("Invalid iterable length"));
let vec: Vec<T, N> = iter_into_vec(iterable)?;
// Returns error if array.len() != N
vec.into_array().map_err(|_| err)
}
pub fn iter_into_vec<T, const N: usize>(iterable: Obj) -> Result<Vec<T, N>, Error>
where
T: TryFrom<Obj, Error = Error>,
{
@ -29,6 +39,5 @@ where
for item in Iter::try_from_obj_with_buf(iterable, &mut iter_buf)? {
vec.push(item.try_into()?).map_err(|_| err)?;
}
// Returns error if array.len() != N
vec.into_array().map_err(|_| err)
Ok(vec)
}

View File

@ -4,3 +4,35 @@ macro_rules! include_res {
include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/ui/", $filename))
};
}
#[allow(unused_macros)] // Only used in TR so far.
/// Concatenates arbitrary amount of slices into a String.
macro_rules! build_string {
($max:expr, $($string:expr),+) => {
{
let mut new_string = String::<$max>::new();
$(new_string.push_str($string).unwrap();)+
new_string
}
}
}
#[allow(unused_macros)] // Mostly for debugging purposes.
/// Transforms integer into string slice. For example for printing.
macro_rules! inttostr {
($int:expr) => {{
heapless::String::<10>::from($int).as_str()
}};
}
#[allow(unused_macros)] // Mostly for debugging purposes.
/// Transforms bool into string slice. For example for printing.
macro_rules! booltostr {
($bool:expr) => {{
if $bool {
"true"
} else {
"false"
}
}};
}

View File

@ -4,11 +4,12 @@ pub mod macros;
pub mod animation;
pub mod component;
pub mod constant;
pub mod debug;
pub mod display;
pub mod event;
pub mod geometry;
pub mod lerp;
mod util;
pub mod util;
#[cfg(feature = "micropython")]
pub mod layout;

View File

@ -132,7 +132,7 @@ where
ButtonContent::Text(text) => {
let background_color = style.text_color.negate();
if style.border_horiz {
display::rect_fill_rounded1(self.area, background_color, theme::BG);
display::rect_fill_rounded(self.area, background_color, theme::BG, 1);
} else {
display::rect_fill(self.area, background_color)
}

View File

@ -59,7 +59,7 @@ where
theme::FG,
theme::BG,
);
display::dotted_line(self.area.bottom_left(), self.area.width(), theme::FG);
display::dotted_line_horizontal(self.area.bottom_left(), self.area.width(), theme::FG, 2);
self.content.paint();
}
}

View File

@ -57,10 +57,10 @@ extern "C" fn new_confirm_action(n_args: usize, args: *const Obj, kwargs: *mut M
let reverse: bool = kwargs.get(Qstr::MP_QSTR_reverse)?.try_into()?;
let format = match (&action, &description, reverse) {
(Some(_), Some(_), false) => "{bold}{action}\n\r{normal}{description}",
(Some(_), Some(_), true) => "{normal}{description}\n\r{bold}{action}",
(Some(_), None, _) => "{bold}{action}",
(None, Some(_), _) => "{normal}{description}",
(Some(_), Some(_), false) => "{Font::bold}{action}\n\r{Font::normal}{description}",
(Some(_), Some(_), true) => "{Font::normal}{description}\n\r{Font::bold}{action}",
(Some(_), None, _) => "{Font::bold}{action}",
(None, Some(_), _) => "{Font::normal}{description}",
_ => "",
};

View File

@ -8,7 +8,6 @@ use super::component::{ButtonStyle, ButtonStyleSheet};
// Color palette.
pub const WHITE: Color = Color::rgb(255, 255, 255);
pub const BLACK: Color = Color::rgb(0, 0, 0);
pub const GREY_LIGHT: Color = WHITE; // Word/page break characters.
pub const FG: Color = WHITE; // Default foreground (text & icon) color.
pub const BG: Color = BLACK; // Default background color.

View File

@ -0,0 +1,282 @@
use crate::{
trezorhal::bip39,
ui::{
component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx},
geometry::Rect,
},
};
use super::{
choice_item::BigCharacterChoiceItem, ButtonDetails, ButtonLayout, ChangingTextLine,
ChoiceFactory, ChoiceItem, ChoicePage, ChoicePageMsg, TextChoiceItem,
};
use heapless::{String, Vec};
pub enum Bip39EntryMsg {
ResultWord(String<15>),
}
const CURRENT_LETTERS_ROW: i32 = 25;
const MAX_LENGTH: usize = 10;
const MAX_CHOICE_LENGTH: usize = 26;
/// Offer words when there will be fewer of them than this
const OFFER_WORDS_THRESHOLD: usize = 10;
struct ChoiceFactoryBIP39 {
// TODO: replace these Vecs by iterators somehow?
letter_choices: Option<Vec<char, MAX_CHOICE_LENGTH>>,
word_choices: Option<Vec<&'static str, OFFER_WORDS_THRESHOLD>>,
}
impl ChoiceFactoryBIP39 {
fn new(
letter_choices: Option<Vec<char, MAX_CHOICE_LENGTH>>,
word_choices: Option<Vec<&'static str, OFFER_WORDS_THRESHOLD>>,
) -> Self {
Self {
letter_choices,
word_choices,
}
}
fn letters(letter_choices: Vec<char, MAX_CHOICE_LENGTH>) -> Self {
Self::new(Some(letter_choices), None)
}
fn words(word_choices: Vec<&'static str, OFFER_WORDS_THRESHOLD>) -> Self {
Self::new(None, Some(word_choices))
}
/// Word choice items with BIN leftmost button.
fn get_word_item(&self, choice_index: u8) -> ChoiceItem {
if let Some(word_choices) = &self.word_choices {
let word = word_choices[choice_index as usize];
let choice = TextChoiceItem::new(word, ButtonLayout::default_three_icons());
let mut word_item = ChoiceItem::Text(choice);
// Adding BIN leftmost button and removing the rightmost one.
if choice_index == 0 {
word_item.set_left_btn(Some(ButtonDetails::bin_icon()));
} else if choice_index as usize == word_choices.len() - 1 {
word_item.set_right_btn(None);
}
word_item
} else {
unreachable!()
}
}
/// Letter choice items with BIN leftmost button. Letters are BIG.
fn get_letter_item(&self, choice_index: u8) -> ChoiceItem {
// TODO: we could support carousel for letters to quicken it for users
// (but then the BIN would need to be an option on its own, not so
// user-friendly)
if let Some(letter_choices) = &self.letter_choices {
let letter = letter_choices[choice_index as usize];
let letter_choice =
BigCharacterChoiceItem::new(letter, ButtonLayout::default_three_icons());
let mut letter_item = ChoiceItem::BigCharacter(letter_choice);
// Adding BIN leftmost button and removing the rightmost one.
if choice_index == 0 {
letter_item.set_left_btn(Some(ButtonDetails::bin_icon()));
} else if choice_index as usize == letter_choices.len() - 1 {
letter_item.set_right_btn(None);
}
letter_item
} else {
unreachable!()
}
}
}
impl ChoiceFactory for ChoiceFactoryBIP39 {
fn get(&self, choice_index: u8) -> ChoiceItem {
if self.letter_choices.is_some() {
self.get_letter_item(choice_index)
} else if self.word_choices.is_some() {
self.get_word_item(choice_index)
} else {
unreachable!()
}
}
fn count(&self) -> u8 {
if let Some(letter_choices) = &self.letter_choices {
letter_choices.len() as u8
} else if let Some(word_choices) = &self.word_choices {
word_choices.len() as u8
} else {
unreachable!()
}
}
}
/// Component for entering a BIP39 mnemonic.
pub struct Bip39Entry {
choice_page: ChoicePage<ChoiceFactoryBIP39>,
chosen_letters: Child<ChangingTextLine<String<{ MAX_LENGTH + 1 }>>>,
letter_choices: Vec<char, MAX_CHOICE_LENGTH>,
textbox: TextBox<MAX_LENGTH>,
offer_words: bool,
words_list: bip39::Wordlist,
}
impl Bip39Entry {
pub fn new() -> Self {
let letter_choices: Vec<char, MAX_CHOICE_LENGTH> =
bip39::get_available_letters("").collect();
let choices = ChoiceFactoryBIP39::letters(letter_choices.clone());
Self {
choice_page: ChoicePage::new(choices),
chosen_letters: Child::new(ChangingTextLine::center_mono(String::new())),
letter_choices,
textbox: TextBox::empty(),
offer_words: false,
words_list: bip39::Wordlist::all(),
}
}
/// Gets up-to-date choices for letters or words.
fn get_current_choices(&mut self) -> ChoiceFactoryBIP39 {
// Narrowing the word list
self.words_list = self.words_list.filter_prefix(self.textbox.content());
// Offering words when there is only a few of them
// Otherwise getting relevant letters
if self.words_list.len() < OFFER_WORDS_THRESHOLD {
self.offer_words = true;
let word_choices = self.words_list.iter().collect();
ChoiceFactoryBIP39::words(word_choices)
} else {
self.offer_words = false;
self.letter_choices = bip39::get_available_letters(self.textbox.content()).collect();
ChoiceFactoryBIP39::letters(self.letter_choices.clone())
}
}
fn update_chosen_letters(&mut self, ctx: &mut EventCtx) {
let text = build_string!({ MAX_LENGTH + 1 }, self.textbox.content(), "_");
self.chosen_letters.inner_mut().update_text(text);
self.chosen_letters.request_complete_repaint(ctx);
}
fn append_letter(&mut self, ctx: &mut EventCtx, letter: char) {
self.textbox.append(ctx, letter);
}
fn delete_last_letter(&mut self, ctx: &mut EventCtx) {
self.textbox.delete_last(ctx);
}
fn reset_wordlist(&mut self) {
self.words_list = bip39::Wordlist::all();
}
}
impl Component for Bip39Entry {
type Msg = Bip39EntryMsg;
fn place(&mut self, bounds: Rect) -> Rect {
let letters_area_height = self.chosen_letters.inner().needed_height();
let (letters_area, choice_area) = bounds.split_top(letters_area_height);
self.chosen_letters.place(letters_area);
self.choice_page.place(choice_area);
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
let msg = self.choice_page.event(ctx, event);
match msg {
Some(ChoicePageMsg::Choice(page_counter)) => {
// Clicked SELECT.
// When we already offer words, return the word at the given index.
// Otherwise, appending the new letter and resetting the choice page
// with up-to-date choices.
if self.offer_words {
let word = self
.words_list
.get(page_counter as usize)
.unwrap_or_default();
return Some(Bip39EntryMsg::ResultWord(String::from(word)));
} else {
let new_letter = self.letter_choices[page_counter as usize];
self.append_letter(ctx, new_letter);
self.update_chosen_letters(ctx);
let new_choices = self.get_current_choices();
self.choice_page.reset(ctx, new_choices, true, false);
ctx.request_paint();
}
}
Some(ChoicePageMsg::LeftMost) => {
// Clicked BIN. Deleting last letter, updating wordlist and updating choices
self.delete_last_letter(ctx);
self.update_chosen_letters(ctx);
self.reset_wordlist();
let new_choices = self.get_current_choices();
self.choice_page.reset(ctx, new_choices, true, false);
ctx.request_paint();
}
_ => {}
}
None
}
fn paint(&mut self) {
self.chosen_letters.paint();
self.choice_page.paint();
}
}
#[cfg(feature = "ui_debug")]
use super::{ButtonAction, ButtonPos};
#[cfg(feature = "ui_debug")]
use crate::ui::util;
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for Bip39Entry {
fn get_btn_action(&self, pos: ButtonPos) -> String<25> {
match pos {
ButtonPos::Left => match self.choice_page.has_previous_choice() {
true => ButtonAction::PrevPage.string(),
false => ButtonAction::Action("Delete last char").string(),
},
ButtonPos::Right => match self.choice_page.has_next_choice() {
true => ButtonAction::NextPage.string(),
false => ButtonAction::empty(),
},
ButtonPos::Middle => {
let current_index = self.choice_page.page_index() as usize;
let choice: String<10> = if self.offer_words {
self.words_list
.get(current_index)
.unwrap_or_default()
.into()
} else {
util::char_to_string(self.letter_choices[current_index])
};
ButtonAction::select_item(choice)
}
}
}
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("Bip39Entry");
t.kw_pair("textbox", self.textbox.content());
self.report_btn_actions(t);
t.open("letter_choices");
for ch in &self.letter_choices {
t.string(&util::char_to_string::<1>(*ch));
}
t.close();
t.field("choice_page", &self.choice_page);
t.close();
}
}

View File

@ -1,19 +1,30 @@
use crate::ui::{
component::{Component, Event, EventCtx},
display::{self, Color, Font},
event::{ButtonEvent, PhysicalButton},
geometry::{Offset, Point, Rect},
use crate::{
time::Duration,
ui::{
component::{Component, Event, EventCtx},
constant,
display::{self, Color, Font, Icon},
event::{ButtonEvent, PhysicalButton},
geometry::{Offset, Point, Rect},
},
};
use heapless::String;
use super::theme;
const HALF_SCREEN_BUTTON_WIDTH: i16 = constant::WIDTH / 2 - 1;
#[derive(Eq, PartialEq)]
pub enum ButtonMsg {
Clicked,
LongPressed,
}
#[derive(Copy, Clone)]
pub enum ButtonPos {
Left,
Middle,
Right,
}
@ -21,15 +32,16 @@ impl ButtonPos {
pub fn hit(&self, b: &PhysicalButton) -> bool {
matches!(
(self, b),
(Self::Left, PhysicalButton::Left) | (Self::Right, PhysicalButton::Right)
(Self::Left, PhysicalButton::Left)
| (Self::Middle, PhysicalButton::Both)
| (Self::Right, PhysicalButton::Right)
)
}
}
pub struct Button<T> {
area: Rect,
bounds: Rect,
pos: ButtonPos,
baseline: Point,
content: ButtonContent<T>,
styles: ButtonStyleSheet,
state: State,
@ -41,8 +53,7 @@ impl<T: AsRef<str>> Button<T> {
pos,
content,
styles,
baseline: Point::zero(),
area: Rect::zero(),
bounds: Rect::zero(),
state: State::Released,
}
}
@ -51,7 +62,7 @@ impl<T: AsRef<str>> Button<T> {
Self::new(pos, ButtonContent::Text(text), styles)
}
pub fn with_icon(pos: ButtonPos, image: &'static [u8], styles: ButtonStyleSheet) -> Self {
pub fn with_icon(pos: ButtonPos, image: Icon, styles: ButtonStyleSheet) -> Self {
Self::new(pos, ButtonContent::Icon(image), styles)
}
@ -59,13 +70,29 @@ impl<T: AsRef<str>> Button<T> {
&self.content
}
fn style(&self) -> &ButtonStyle {
fn style(&self) -> ButtonStyle {
match self.state {
State::Released => self.styles.normal,
State::Pressed => self.styles.active,
}
}
/// Changing the icon content of the button.
pub fn set_icon(&mut self, image: Icon) {
self.content = ButtonContent::Icon(image);
}
/// Changing the text content of the button.
pub fn set_text(&mut self, text: T) {
self.content = ButtonContent::Text(text);
}
/// Changing the style of the button.
pub fn set_style(&mut self, styles: ButtonStyleSheet) {
self.styles = styles;
}
// Setting the visual state of the button.
fn set(&mut self, ctx: &mut EventCtx, state: State) {
if self.state != state {
self.state = state;
@ -73,26 +100,75 @@ impl<T: AsRef<str>> Button<T> {
}
}
fn placement(
area: Rect,
pos: ButtonPos,
content: &ButtonContent<T>,
styles: &ButtonStyleSheet,
) -> (Rect, Point) {
let border_width = if styles.normal.border_horiz { 2 } else { 0 };
let content_width = match content {
ButtonContent::Text(text) => styles.normal.font.text_width(text.as_ref()) - 1,
ButtonContent::Icon(_icon) => todo!(),
// Setting the visual state of the button.
pub fn set_pressed(&mut self, ctx: &mut EventCtx, is_pressed: bool) {
let new_state = if is_pressed {
State::Pressed
} else {
State::Released
};
let button_width = content_width + 2 * border_width;
let area = match pos {
ButtonPos::Left => area.split_left(button_width).0,
ButtonPos::Right => area.split_right(button_width).1,
self.set(ctx, new_state);
}
/// Return the full area of the button according
/// to its current style, content and position.
fn get_current_area(&self) -> Rect {
let style = self.style();
// Button width may be forced. Otherwise calculate it.
let button_width = if let Some(width) = style.force_width {
width
} else {
let outline = if style.with_outline {
theme::BUTTON_OUTLINE
} else {
0
};
let content_width = match &self.content {
ButtonContent::Text(text) => style.font.text_width(text.as_ref()) - 1,
ButtonContent::Icon(icon) => icon.width() - 1,
};
content_width + 2 * outline
};
let start_of_baseline = area.bottom_left() + Offset::new(border_width, -2);
// Button height may be adjusted for the icon without outline
// Done to avoid highlighting bigger area than necessary when
// drawing the icon in active (black on white) state
let button_height = match &self.content {
ButtonContent::Text(_) => theme::BUTTON_HEIGHT,
ButtonContent::Icon(icon) => {
if style.with_outline {
theme::BUTTON_HEIGHT
} else {
icon.height()
}
}
};
(area, start_of_baseline)
let button_bounds = self.bounds.split_bottom(button_height).1;
let area = match self.pos {
ButtonPos::Left => button_bounds.split_left(button_width).0,
ButtonPos::Right => button_bounds.split_right(button_width).1,
ButtonPos::Middle => button_bounds.split_center(button_width).1,
};
// Allowing for possible offset of the area from current style
if let Some(offset) = style.offset {
area.translate(offset)
} else {
area
}
}
/// Determine baseline point for the text.
fn get_text_baseline(&self, style: &ButtonStyle) -> Point {
// Arms and outline require the text to be elevated.
if style.with_arms || style.with_outline {
let offset = theme::BUTTON_OUTLINE;
self.get_current_area().bottom_left() + Offset::new(offset, -offset)
} else {
self.get_current_area().bottom_left()
}
}
}
@ -103,13 +179,13 @@ where
type Msg = ButtonMsg;
fn place(&mut self, bounds: Rect) -> Rect {
let (area, baseline) = Self::placement(bounds, self.pos, &self.content, &self.styles);
self.area = area;
self.baseline = baseline;
self.area
self.bounds = bounds;
self.get_current_area()
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
// Everything should be handled by `ButtonController`
// TODO: could be completely deleted, but `ResultPopup` is using Button.event()
match event {
Event::Button(ButtonEvent::ButtonPressed(which)) if self.pos.hit(&which) => {
self.set(ctx, State::Pressed);
@ -127,26 +203,74 @@ where
fn paint(&mut self) {
let style = self.style();
let text_color = style.text_color;
let background_color = text_color.negate();
let area = self.get_current_area();
// TODO: support another combinations of text and icons
// - text with OK icon on left
// Optionally display "arms" at both sides of content, or create
// a nice rounded outline around it.
// By default just fill the content background.
if style.with_arms {
// Prepare space for both the arms and content with BG color.
// Arms are icons 10*6 pixels.
let area_to_fill = area.extend_left(10).extend_right(15);
display::rect_fill(area_to_fill, background_color);
// Paint both arms.
// Baselines need to be shifted little bit right to fit properly with the text
// TODO: for "CONFIRM" there is one space at the right, but for "SELECT" there
// are two
Icon::new(theme::ICON_ARM_LEFT).draw_top_right(
area.left_center() + Offset::x(2),
text_color,
background_color,
);
Icon::new(theme::ICON_ARM_RIGHT).draw_top_left(
area.right_center() + Offset::x(4),
text_color,
background_color,
);
} else if style.with_outline {
display::rect_outline_rounded(area, text_color, background_color, 2);
} else {
display::rect_fill(area, background_color)
}
match &self.content {
ButtonContent::Text(text) => {
let background_color = style.text_color.negate();
if style.border_horiz {
display::rect_fill_rounded1(self.area, background_color, theme::BG);
} else {
display::rect_fill(self.area, background_color)
}
display::text(
self.baseline,
self.get_text_baseline(&style),
text.as_ref(),
style.font,
style.text_color,
text_color,
background_color,
);
}
ButtonContent::Icon(_image) => {
todo!();
ButtonContent::Icon(icon) => {
if style.with_outline {
// Accounting for the 8*8 icon with empty left column and bottom row
// (which fits the outline nicely and symmetrically)
let center = area.center() + Offset::uniform(1);
icon.draw_center(center, text_color, background_color);
} else {
// Positioning the icon in the corresponding corner/center
match self.pos {
ButtonPos::Left => {
icon.draw_bottom_left(area.bottom_left(), text_color, background_color)
}
ButtonPos::Right => icon.draw_bottom_right(
area.bottom_right(),
text_color,
background_color,
),
ButtonPos::Middle => {
icon.draw_center(area.center(), text_color, background_color)
}
}
}
}
}
}
@ -161,7 +285,7 @@ where
t.open("Button");
match &self.content {
ButtonContent::Text(text) => t.field("text", text),
ButtonContent::Icon(_) => t.symbol("icon"),
ButtonContent::Icon(icon) => t.field("icon", icon),
}
t.close();
}
@ -175,16 +299,486 @@ enum State {
pub enum ButtonContent<T> {
Text(T),
Icon(&'static [u8]),
Icon(Icon),
}
pub struct ButtonStyleSheet {
pub normal: &'static ButtonStyle,
pub active: &'static ButtonStyle,
pub normal: ButtonStyle,
pub active: ButtonStyle,
}
#[derive(Clone, Copy)]
pub struct ButtonStyle {
pub font: Font,
pub text_color: Color,
pub border_horiz: bool,
pub with_outline: bool,
pub with_arms: bool,
pub force_width: Option<i16>,
pub offset: Option<Offset>,
}
// TODO: currently `button_default` and `button_cancel`
// are the same - decide whether to differentiate them.
// In Figma, they are not differentiated.
impl ButtonStyleSheet {
pub fn new(
normal_color: Color,
active_color: Color,
with_outline: bool,
with_arms: bool,
force_width: Option<i16>,
offset: Option<Offset>,
) -> Self {
Self {
normal: ButtonStyle {
font: theme::FONT_BUTTON,
text_color: normal_color,
with_outline,
with_arms,
force_width,
offset,
},
active: ButtonStyle {
font: theme::FONT_BUTTON,
text_color: active_color,
with_outline,
with_arms,
force_width,
offset,
},
}
}
// White text in normal mode.
pub fn default(
with_outline: bool,
with_arms: bool,
force_width: Option<i16>,
offset: Option<Offset>,
) -> Self {
Self::new(
theme::FG,
theme::BG,
with_outline,
with_arms,
force_width,
offset,
)
}
// Black text in normal mode.
pub fn cancel(
with_outline: bool,
with_arms: bool,
force_width: Option<i16>,
offset: Option<Offset>,
) -> Self {
Self::new(
theme::FG,
theme::BG,
with_outline,
with_arms,
force_width,
offset,
)
// Self::new(theme::BG, theme::FG, with_outline, with_arms)
}
}
/// Describing the button in the choice item.
#[derive(Clone, Copy)]
pub struct ButtonDetails<T> {
pub text: Option<T>,
pub icon: Option<Icon>,
pub duration: Option<Duration>,
pub is_cancel: bool,
pub with_outline: bool,
pub with_arms: bool,
pub force_width: Option<i16>,
pub offset: Option<Offset>,
}
impl<T: Clone + AsRef<str>> ButtonDetails<T> {
/// Text button.
pub fn text(text: T) -> Self {
Self {
text: Some(text),
icon: None,
duration: None,
is_cancel: false,
with_outline: true,
with_arms: false,
force_width: None,
offset: None,
}
}
/// Icon button.
pub fn icon(icon: Icon) -> Self {
Self {
text: None,
icon: Some(icon),
duration: None,
is_cancel: false,
with_outline: true,
with_arms: false,
force_width: None,
offset: None,
}
}
/// Text with arms signalling double press.
pub fn armed_text(text: T) -> Self {
Self::text(text).with_arms()
}
/// Cross-style-icon cancel button with no outline.
pub fn cancel_icon() -> Self {
Self::icon(Icon::new(theme::ICON_CANCEL))
.with_no_outline()
.with_offset(Offset::new(2, -2))
}
/// Left arrow to signal going back.
pub fn left_arrow_icon() -> Self {
Self::icon(Icon::new(theme::ICON_ARROW_LEFT)).with_no_outline()
}
/// Right arrow to signal going forward.
pub fn right_arrow_icon() -> Self {
Self::icon(Icon::new(theme::ICON_ARROW_RIGHT)).with_no_outline()
}
/// Down arrow to signal paginating forward. Takes half the screen's width
pub fn down_arrow_icon_wide() -> Self {
Self::icon(Icon::new(theme::ICON_ARROW_DOWN)).force_width(HALF_SCREEN_BUTTON_WIDTH)
}
/// Up arrow to signal paginating back. Takes half the screen's width
pub fn up_arrow_icon_wide() -> Self {
Self::icon(Icon::new(theme::ICON_ARROW_UP)).force_width(HALF_SCREEN_BUTTON_WIDTH)
}
/// Icon of a bin to signal deleting.
pub fn bin_icon() -> Self {
Self::icon(Icon::new(theme::ICON_BIN)).with_no_outline()
}
/// Cancel style button.
pub fn with_cancel(mut self) -> Self {
self.is_cancel = true;
self
}
/// No outline around the button.
pub fn with_no_outline(mut self) -> Self {
self.with_outline = false;
self
}
/// Positioning the icon precisely where we want.
/// Buttons are by default placed exactly in the corners (left/right)
/// or in the center in case of center button. The offset can change it.
pub fn with_offset(mut self, offset: Offset) -> Self {
self.offset = Some(offset);
self
}
/// Left and right "arms" around the button.
/// Automatically disabling the outline.
pub fn with_arms(mut self) -> Self {
self.with_arms = true;
self.with_outline = false;
self
}
/// Duration of the hold-to-confirm.
pub fn with_duration(mut self, duration: Duration) -> Self {
self.duration = Some(duration);
self
}
/// Width of the button.
pub fn force_width(mut self, width: i16) -> Self {
self.force_width = Some(width);
self
}
/// Button style that should be applied.
pub fn style(&self) -> ButtonStyleSheet {
if self.is_cancel {
ButtonStyleSheet::cancel(
self.with_outline,
self.with_arms,
self.force_width,
self.offset,
)
} else {
ButtonStyleSheet::default(
self.with_outline,
self.with_arms,
self.force_width,
self.offset,
)
}
}
}
/// Holding the button details for all three possible buttons.
#[derive(Clone)]
pub struct ButtonLayout<T> {
pub btn_left: Option<ButtonDetails<T>>,
pub btn_middle: Option<ButtonDetails<T>>,
pub btn_right: Option<ButtonDetails<T>>,
}
impl<T: AsRef<str>> ButtonLayout<T> {
pub fn new(
btn_left: Option<ButtonDetails<T>>,
btn_middle: Option<ButtonDetails<T>>,
btn_right: Option<ButtonDetails<T>>,
) -> Self {
Self {
btn_left,
btn_middle,
btn_right,
}
}
/// Empty layout for when we cannot yet tell which buttons
/// should be on the screen.
pub fn empty() -> Self {
Self::new(None, None, None)
}
}
impl ButtonLayout<&'static str> {
/// Default button layout for all three buttons - icons.
pub fn default_three_icons() -> Self {
Self::three_icons_middle_text("SELECT")
}
/// Special middle text for default icon layout.
pub fn three_icons_middle_text(middle_text: &'static str) -> Self {
Self::new(
Some(ButtonDetails::left_arrow_icon()),
Some(ButtonDetails::armed_text(middle_text)),
Some(ButtonDetails::right_arrow_icon()),
)
}
/// Left and right arrow icons for navigation.
pub fn left_right_arrows() -> Self {
Self::new(
Some(ButtonDetails::left_arrow_icon()),
None,
Some(ButtonDetails::right_arrow_icon()),
)
}
/// Cancel cross on left and right arrow.
pub fn cancel_and_arrow() -> Self {
Self::new(
Some(ButtonDetails::cancel_icon()),
None,
Some(ButtonDetails::right_arrow_icon()),
)
}
/// Cancel cross on left and text on the right.
pub fn cancel_and_text(text: &'static str) -> Self {
Self::new(
Some(ButtonDetails::cancel_icon()),
None,
Some(ButtonDetails::text(text)),
)
}
/// Cancel cross on left and hold-to-confirm text on the right.
pub fn cancel_and_htc_text(text: &'static str, duration: Duration) -> Self {
Self::new(
Some(ButtonDetails::cancel_icon()),
None,
Some(ButtonDetails::text(text).with_duration(duration)),
)
}
}
/// What happens when a button is triggered.
/// Theoretically any action can be connected
/// with any button.
#[derive(Clone, PartialEq, Eq)]
pub enum ButtonAction {
/// Go to the next page of this flow
NextPage,
/// Go to the previous page of this flow
PrevPage,
/// Go to a page of this flow specified by an index.
/// Negative numbers can be used to count from the end.
/// (0 ~ GoToFirstPage, -1 ~ GoToLastPage etc.)
GoToIndex(i16),
/// Go forwards/backwards a specified number of pages.
/// Negative numbers mean going back.
MovePageRelative(i16),
/// Cancel the whole layout - send Msg::Cancelled
Cancel,
/// Confirm the whole layout - send Msg::Confirmed
Confirm,
/// Select current choice value
Select,
/// Some custom specific action
Action(&'static str),
}
#[cfg(feature = "ui_debug")]
impl ButtonAction {
/// Describing the action as a string. Debug-only.
pub fn string(&self) -> String<25> {
match self {
ButtonAction::NextPage => "Next".into(),
ButtonAction::PrevPage => "Prev".into(),
ButtonAction::GoToIndex(index) => {
build_string!(25, "Index(", inttostr!(*index), ")")
}
ButtonAction::MovePageRelative(index) => {
build_string!(25, "Relative(", inttostr!(*index), ")")
}
ButtonAction::Cancel => "Cancel".into(),
ButtonAction::Confirm => "Confirm".into(),
ButtonAction::Select => "Select".into(),
ButtonAction::Action(action) => (*action).into(),
}
}
/// Adding a description to the Select action.
pub fn select_item<T: AsRef<str>>(item: T) -> String<25> {
build_string!(25, &Self::Select.string(), "(", item.as_ref(), ")")
}
/// When there is no action.
pub fn empty() -> String<25> {
"None".into()
}
}
// TODO: might consider defining ButtonAction::Empty
// and only storing ButtonAction instead of Option<ButtonAction>...
/// Storing actions for all three possible buttons.
#[derive(Clone)]
pub struct ButtonActions {
pub left: Option<ButtonAction>,
pub middle: Option<ButtonAction>,
pub right: Option<ButtonAction>,
}
impl ButtonActions {
pub fn new(
left: Option<ButtonAction>,
middle: Option<ButtonAction>,
right: Option<ButtonAction>,
) -> Self {
Self {
left,
middle,
right,
}
}
/// Going back with left, going further with right
pub fn prev_next() -> Self {
Self::new(
Some(ButtonAction::PrevPage),
None,
Some(ButtonAction::NextPage),
)
}
/// Going back with left, going further with middle
pub fn prev_next_with_middle() -> Self {
Self::new(
Some(ButtonAction::PrevPage),
Some(ButtonAction::NextPage),
None,
)
}
/// Going to last page with left, to the next page with right
pub fn last_next() -> Self {
Self::new(
Some(ButtonAction::GoToIndex(-1)),
None,
Some(ButtonAction::NextPage),
)
}
/// Cancelling with left, going to the next page with right
pub fn cancel_next() -> Self {
Self::new(
Some(ButtonAction::Cancel),
None,
Some(ButtonAction::NextPage),
)
}
/// Cancelling with left, confirming with right
pub fn cancel_confirm() -> Self {
Self::new(
Some(ButtonAction::Cancel),
None,
Some(ButtonAction::Confirm),
)
}
/// Going to the beginning with left, confirming with right
pub fn beginning_confirm() -> Self {
Self::new(
Some(ButtonAction::GoToIndex(0)),
None,
Some(ButtonAction::Confirm),
)
}
/// Going to the beginning with left, cancelling with right
pub fn beginning_cancel() -> Self {
Self::new(
Some(ButtonAction::GoToIndex(0)),
None,
Some(ButtonAction::Cancel),
)
}
/// Having access to appropriate action based on the `ButtonPos`
pub fn get_action(&self, pos: ButtonPos) -> Option<ButtonAction> {
match pos {
ButtonPos::Left => self.left.clone(),
ButtonPos::Middle => self.middle.clone(),
ButtonPos::Right => self.right.clone(),
}
}
}
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for ButtonDetails<T>
where
T: AsRef<str>,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("ButtonDetails");
let mut btn_text: String<30> = String::new();
if let Some(text) = &self.text {
btn_text.push_str(text.as_ref()).unwrap();
} else if let Some(icon) = &self.icon {
btn_text.push_str("Icon:").unwrap();
btn_text.push_str(icon.text.as_ref()).unwrap();
}
if let Some(duration) = &self.duration {
btn_text.push_str(" (HTC:").unwrap();
btn_text.push_str(inttostr!(duration.to_millis())).unwrap();
btn_text.push_str(")").unwrap();
}
t.button(btn_text.as_ref());
t.close();
}
}

View File

@ -0,0 +1,449 @@
use super::{
theme, Button, ButtonDetails, ButtonLayout, ButtonPos, HoldToConfirm, HoldToConfirmMsg,
LoaderStyleSheet,
};
use crate::{
time::Duration,
ui::{
component::{base::Event, Component, EventCtx, Pad},
event::{ButtonEvent, PhysicalButton},
geometry::Rect,
},
};
use heapless::String;
#[derive(Copy, Clone, PartialEq, Eq)]
enum ButtonState {
Nothing,
OneDown(PhysicalButton),
BothDown,
OneReleased(PhysicalButton),
HTCNeedsRelease(PhysicalButton),
}
pub enum ButtonControllerMsg {
Triggered(ButtonPos),
}
/// Defines what kind of button should be currently used.
pub enum ButtonType<T> {
Button(Button<T>),
HoldToConfirm(HoldToConfirm<T>),
Nothing,
}
impl<T> ButtonType<T>
where
T: AsRef<str>,
T: Clone,
{
pub fn from_button_details(pos: ButtonPos, btn_details: Option<ButtonDetails<T>>) -> Self {
if let Some(btn_details) = btn_details {
if btn_details.duration.is_some() {
Self::HoldToConfirm(Self::get_hold_to_confirm(pos, btn_details))
} else {
Self::Button(Self::get_button(pos, btn_details))
}
} else {
Self::Nothing
}
}
/// Create `Button` component from `btn_details`.
fn get_button(pos: ButtonPos, btn_details: ButtonDetails<T>) -> Button<T> {
// Deciding between text and icon
if let Some(text) = btn_details.clone().text {
Button::with_text(pos, text, btn_details.style())
} else if let Some(icon) = btn_details.icon {
Button::with_icon(pos, icon, btn_details.style())
} else {
panic!("ButtonContainer: no text or icon provided");
}
}
/// Create `HoldToConfirm` component from `btn_details`.
fn get_hold_to_confirm(pos: ButtonPos, btn_details: ButtonDetails<T>) -> HoldToConfirm<T> {
let duration = btn_details
.duration
.unwrap_or_else(|| Duration::from_millis(1000));
if let Some(text) = btn_details.text {
HoldToConfirm::text(pos, text, LoaderStyleSheet::default(), duration)
} else if let Some(icon) = btn_details.icon {
HoldToConfirm::icon(pos, icon, LoaderStyleSheet::default(), duration)
} else {
panic!("ButtonContainer: no text or icon provided");
}
}
pub fn place(&mut self, button_area: Rect) {
match self {
Self::Button(button) => {
button.place(button_area);
}
Self::HoldToConfirm(htc) => {
htc.place(button_area);
}
Self::Nothing => {}
}
}
pub fn paint(&mut self) {
match self {
Self::Button(button) => {
button.paint();
}
Self::HoldToConfirm(htc) => {
htc.paint();
}
Self::Nothing => {}
}
}
}
/// Wrapping a button and its state, so that it can be easily
/// controlled from outside.
///
/// Users have a choice of a normal button or Hold-to-confirm button.
/// `button_type` specified what from those two is used, if anything.
pub struct ButtonContainer<T> {
pos: ButtonPos,
button_type: ButtonType<T>,
}
impl<T: Clone + AsRef<str>> ButtonContainer<T> {
/// Supplying `None` as `btn_details` marks the button inactive
/// (it can be later activated in `set()`).
pub fn new(pos: ButtonPos, btn_details: Option<ButtonDetails<T>>) -> Self {
Self {
pos,
button_type: ButtonType::from_button_details(pos, btn_details),
}
}
/// Changing the state of the button.
///
/// Passing `None` as `btn_details` will mark the button as inactive.
pub fn set(&mut self, btn_details: Option<ButtonDetails<T>>, button_area: Rect) {
self.button_type = ButtonType::from_button_details(self.pos, btn_details);
self.button_type.place(button_area);
}
/// Placing the possible component.
pub fn place(&mut self, bounds: Rect) {
self.button_type.place(bounds);
}
/// Painting the component that should be currently visible, if any.
pub fn paint(&mut self) {
self.button_type.paint();
}
/// Setting the visual state of the button - released/pressed.
pub fn set_pressed(&mut self, ctx: &mut EventCtx, is_pressed: bool) {
if let ButtonType::Button(btn) = &mut self.button_type {
btn.set_pressed(ctx, is_pressed);
}
}
/// Whether single-click should trigger action.
pub fn reacts_to_single_click(&self) -> bool {
matches!(self.button_type, ButtonType::Button(_))
}
/// Find out whether hold-to-confirm was triggered.
pub fn htc_got_triggered(&mut self, ctx: &mut EventCtx, event: Event) -> bool {
if let ButtonType::HoldToConfirm(htc) = &mut self.button_type {
if matches!(htc.event(ctx, event), Some(HoldToConfirmMsg::Confirmed)) {
return true;
}
}
false
}
/// Registering hold event.
pub fn hold_started(&mut self, ctx: &mut EventCtx) {
if let ButtonType::HoldToConfirm(htc) = &mut self.button_type {
htc.event(ctx, Event::Button(ButtonEvent::HoldStarted));
}
}
/// Cancelling hold event.
pub fn hold_ended(&mut self, ctx: &mut EventCtx) {
if let ButtonType::HoldToConfirm(htc) = &mut self.button_type {
htc.event(ctx, Event::Button(ButtonEvent::HoldEnded));
}
}
}
/// Component responsible for handling buttons.
///
/// Acts as a state-machine of `ButtonState`.
///
/// Storing all three possible buttons - left, middle and right -
/// and handling their placement, painting and returning
/// appropriate events when they are triggered.
///
/// Buttons can be interactively changed by clients by `set()`.
///
/// Only "final" button events are returned in `ButtonControllerMsg::Triggered`,
/// based upon the buttons being long-press or not.
pub struct ButtonController<T> {
pad: Pad,
left_btn: ButtonContainer<T>,
middle_btn: ButtonContainer<T>,
right_btn: ButtonContainer<T>,
state: ButtonState,
// Button area is needed so the buttons
// can be "re-placed" after their text is changed
// Will be set in `place`
button_area: Rect,
}
impl<T: Clone + AsRef<str>> ButtonController<T> {
pub fn new(btn_layout: ButtonLayout<T>) -> Self {
Self {
pad: Pad::with_background(theme::BG).with_clear(),
left_btn: ButtonContainer::new(ButtonPos::Left, btn_layout.btn_left),
middle_btn: ButtonContainer::new(ButtonPos::Middle, btn_layout.btn_middle),
right_btn: ButtonContainer::new(ButtonPos::Right, btn_layout.btn_right),
state: ButtonState::Nothing,
button_area: Rect::zero(),
}
}
/// Updating all the three buttons to the wanted states.
pub fn set(&mut self, btn_layout: ButtonLayout<T>) {
self.pad.clear();
self.left_btn.set(btn_layout.btn_left, self.button_area);
self.middle_btn.set(btn_layout.btn_middle, self.button_area);
self.right_btn.set(btn_layout.btn_right, self.button_area);
}
/// Setting the pressed state for all three buttons by boolean flags.
fn set_pressed(&mut self, ctx: &mut EventCtx, left: bool, mid: bool, right: bool) {
self.left_btn.set_pressed(ctx, left);
self.middle_btn.set_pressed(ctx, mid);
self.right_btn.set_pressed(ctx, right);
}
/// Handle middle button hold-to-confirm start.
/// We need to cancel possible holds in both other buttons.
fn middle_hold_started(&mut self, ctx: &mut EventCtx) {
self.left_btn.hold_ended(ctx);
self.middle_btn.hold_started(ctx);
self.right_btn.hold_ended(ctx);
}
/// Handling the expiration of HTC elements.
/// Finding out if they have been triggered and sending event
/// for the appropriate button.
/// Setting the state to wait for the appropriate release event
/// from the pressed button. Also resetting visible state.
fn handle_htc_expiration(
&mut self,
ctx: &mut EventCtx,
event: Event,
) -> Option<ButtonControllerMsg> {
if self.left_btn.htc_got_triggered(ctx, event) {
self.state = ButtonState::HTCNeedsRelease(PhysicalButton::Left);
self.set_pressed(ctx, false, false, false);
return Some(ButtonControllerMsg::Triggered(ButtonPos::Left));
} else if self.middle_btn.htc_got_triggered(ctx, event) {
// TODO: how to handle it here? Do we even need to?
self.state = ButtonState::Nothing;
self.set_pressed(ctx, false, false, false);
return Some(ButtonControllerMsg::Triggered(ButtonPos::Middle));
} else if self.right_btn.htc_got_triggered(ctx, event) {
self.state = ButtonState::HTCNeedsRelease(PhysicalButton::Right);
self.set_pressed(ctx, false, false, false);
return Some(ButtonControllerMsg::Triggered(ButtonPos::Right));
}
None
}
}
impl<T: Clone + AsRef<str>> Component for ButtonController<T> {
type Msg = ButtonControllerMsg;
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
// State machine for the ButtonController
// We are matching event with `Event::Button` for a button action
// and `Event::Timer` for getting the expiration of HTC.
match event {
Event::Button(button) => {
let (new_state, event) = match self.state {
ButtonState::Nothing => match button {
ButtonEvent::ButtonPressed(which) => {
match which {
PhysicalButton::Left => {
self.left_btn.hold_started(ctx);
}
PhysicalButton::Right => {
self.right_btn.hold_started(ctx);
}
_ => {}
}
(ButtonState::OneDown(which), None)
}
_ => (self.state, None),
},
ButtonState::OneDown(which_down) => match button {
ButtonEvent::ButtonReleased(b) if b == which_down => match which_down {
PhysicalButton::Left => (
ButtonState::Nothing,
if self.left_btn.reacts_to_single_click() {
Some(ButtonControllerMsg::Triggered(ButtonPos::Left))
} else {
self.left_btn.hold_ended(ctx);
None
},
),
PhysicalButton::Right => (
ButtonState::Nothing,
if self.right_btn.reacts_to_single_click() {
Some(ButtonControllerMsg::Triggered(ButtonPos::Right))
} else {
self.right_btn.hold_ended(ctx);
None
},
),
_ => (ButtonState::Nothing, None),
},
ButtonEvent::ButtonPressed(b) if b != which_down => {
self.middle_hold_started(ctx);
(ButtonState::BothDown, None)
}
_ => (self.state, None),
},
ButtonState::BothDown => match button {
ButtonEvent::ButtonReleased(b) => {
self.middle_btn.hold_ended(ctx);
(ButtonState::OneReleased(b), None)
}
_ => (self.state, None),
},
ButtonState::OneReleased(which_up) => match button {
ButtonEvent::ButtonPressed(b) if b == which_up => {
self.middle_hold_started(ctx);
(ButtonState::BothDown, None)
}
ButtonEvent::ButtonReleased(b) if b != which_up => (
ButtonState::Nothing,
if self.middle_btn.reacts_to_single_click() {
Some(ButtonControllerMsg::Triggered(ButtonPos::Middle))
} else {
None
},
),
_ => (self.state, None),
},
ButtonState::HTCNeedsRelease(needs_release) => match button {
// Only going out of this state if correct button was released
ButtonEvent::ButtonReleased(released) if needs_release == released => {
(ButtonState::Nothing, None)
}
_ => (self.state, None),
},
};
// Updating the visual feedback for the buttons
match new_state {
// Not showing anything also when we wait for a release
ButtonState::Nothing | ButtonState::HTCNeedsRelease(_) => {
self.set_pressed(ctx, false, false, false);
}
ButtonState::OneDown(down_button) => match down_button {
PhysicalButton::Left => {
self.set_pressed(ctx, true, false, false);
}
PhysicalButton::Right => {
self.set_pressed(ctx, false, false, true);
}
_ => {}
},
ButtonState::BothDown | ButtonState::OneReleased(_) => {
self.set_pressed(ctx, false, true, false);
}
};
self.state = new_state;
event
}
Event::Timer(_) => self.handle_htc_expiration(ctx, event),
_ => None,
}
}
fn paint(&mut self) {
self.pad.paint();
self.left_btn.paint();
self.middle_btn.paint();
self.right_btn.paint();
}
fn place(&mut self, bounds: Rect) -> Rect {
// Saving button area so that we can re-place the buttons
// when they get updated
self.button_area = bounds;
self.pad.place(bounds);
self.left_btn.place(bounds);
self.middle_btn.place(bounds);
self.right_btn.place(bounds);
bounds
}
}
#[cfg(feature = "ui_debug")]
use super::ButtonContent;
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for ButtonContainer<T>
where
T: AsRef<str>,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("ButtonContainer");
// Putting together text representation of the button
let mut btn_text: String<30> = String::new();
if let ButtonType::Button(btn) = &self.button_type {
match btn.content() {
ButtonContent::Text(text) => {
unwrap!(btn_text.push_str(text.as_ref()));
}
ButtonContent::Icon(icon) => {
unwrap!(btn_text.push_str("Icon:"));
unwrap!(btn_text.push_str(icon.text));
}
}
} else if let ButtonType::HoldToConfirm(htc) = &self.button_type {
unwrap!(btn_text.push_str(htc.get_text().as_ref()));
unwrap!(btn_text.push_str(" (HTC:"));
unwrap!(btn_text.push_str(inttostr!(htc.get_duration().to_millis())));
unwrap!(btn_text.push_str(")"));
} else {
unwrap!(btn_text.push_str(crate::trace::EMPTY_BTN));
}
t.button(btn_text.as_ref());
t.close();
}
}
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for ButtonController<T>
where
T: AsRef<str>,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("ButtonController");
t.field("left_btn", &self.left_btn);
t.field("middle_btn", &self.middle_btn);
t.field("right_btn", &self.right_btn);
t.close();
}
}

View File

@ -0,0 +1,95 @@
use crate::ui::{
component::{Component, Event, EventCtx, Never, Pad},
display::Font,
geometry::{Point, Rect},
};
use super::{common, flow_pages_poc_helpers::LineAlignment, theme};
/// Component that allows for "allocating" a standalone line of text anywhere
/// on the screen and updating it arbitrarily - without affecting the rest
/// and without being affected by other components.
pub struct ChangingTextLine<T> {
area: Rect,
pad: Pad,
text: T,
font: Font,
line_alignment: LineAlignment,
}
impl<T> ChangingTextLine<T>
where
T: AsRef<str>,
{
pub fn new(text: T, font: Font, line_alignment: LineAlignment) -> Self {
Self {
area: Rect::zero(),
pad: Pad::with_background(theme::BG),
text,
font,
line_alignment,
}
}
pub fn center_mono(text: T) -> Self {
Self::new(text, Font::MONO, LineAlignment::Center)
}
pub fn update_text(&mut self, text: T) {
self.text = text;
self.pad.clear();
}
/// Gets the height that is needed for this line to fit perfectly
/// without affecting the rest of the screen.
/// (Accounting for letters that go below the baseline (y, j, ...).)
pub fn needed_height(&self) -> i16 {
self.font.line_height() + 2
}
/// Y coordinate of text baseline, is the same for all paints.
fn y_baseline(&self) -> i16 {
self.area.y0 + self.font.line_height()
}
fn paint_left(&self) {
let baseline = Point::new(self.area.x0, self.y_baseline());
common::display(baseline, &self.text, self.font)
}
fn paint_center(&self) {
let baseline = Point::new(self.area.bottom_center().x, self.y_baseline());
common::display_center(baseline, &self.text, self.font)
}
fn paint_right(&self) {
let baseline = Point::new(self.area.x1, self.y_baseline());
common::display_right(baseline, &self.text, self.font)
}
}
impl<T> Component for ChangingTextLine<T>
where
T: AsRef<str>,
{
type Msg = Never;
fn place(&mut self, bounds: Rect) -> Rect {
self.area = bounds;
self.pad.place(bounds);
bounds
}
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
None
}
fn paint(&mut self) {
self.pad.paint();
match self.line_alignment {
LineAlignment::Left => self.paint_left(),
LineAlignment::Center => self.paint_center(),
LineAlignment::Right => self.paint_right(),
}
}
}

View File

@ -0,0 +1,326 @@
use crate::ui::{
component::{Child, Component, Event, EventCtx, Pad},
geometry::Rect,
};
use super::{theme, ButtonController, ButtonControllerMsg, ButtonPos, ChoiceItem, ChoiceItemAPI};
pub enum ChoicePageMsg {
Choice(u8),
LeftMost,
RightMost,
}
const MIDDLE_ROW: i32 = 72;
/// Interface for a specific component efficiently giving
/// `ChoicePage` all the information it needs to render
/// all the choice pages.
///
/// It avoids the need to store the whole sequence of
/// `ChoiceItem`s in `heapless::Vec` (which caused StackOverflow),
/// but offers a "lazy-loading" way of requesting the
/// `ChoiceItem`s only when they are needed, one-by-one.
/// This way, no more than one `ChoiceItem` is stored in memory at any time.
pub trait ChoiceFactory {
fn get(&self, choice_index: u8) -> ChoiceItem;
fn count(&self) -> u8;
}
/// General component displaying a set of items on the screen
/// and allowing the user to select one of them.
///
/// To be used by other more specific components that will
/// supply a set of `ChoiceItem`s (through `ChoiceFactory`)
/// and will receive back the index of the selected choice.
///
/// Each `ChoiceItem` is responsible for setting the screen -
/// choosing the button text, their duration, text displayed
/// on screen etc.
///
/// `is_carousel` can be used to make the choice page "infinite" -
/// after reaching one end, users will appear at the other end.
pub struct ChoicePage<F>
where
F: ChoiceFactory,
{
choices: F,
pad: Pad,
buttons: Child<ButtonController<&'static str>>,
page_counter: u8,
is_carousel: bool,
}
impl<F> ChoicePage<F>
where
F: ChoiceFactory,
{
pub fn new(choices: F) -> Self {
let initial_btn_layout = choices.get(0).btn_layout();
Self {
choices,
pad: Pad::with_background(theme::BG),
buttons: Child::new(ButtonController::new(initial_btn_layout)),
page_counter: 0,
is_carousel: false,
}
}
/// Set the page counter at the very beginning.
pub fn with_initial_page_counter(mut self, page_counter: u8) -> Self {
self.page_counter = page_counter;
self
}
/// Enabling the carousel mode.
pub fn with_carousel(mut self) -> Self {
self.is_carousel = true;
self
}
/// Resetting the component, which enables reusing the same instance
/// for multiple choice categories.
///
/// NOTE: from the client point of view, it would also be an option to
/// always create a new instance with fresh setup, but I could not manage to
/// properly clean up the previous instance - it would still be shown on
/// screen and colliding with the new one.
pub fn reset(
&mut self,
ctx: &mut EventCtx,
new_choices: F,
reset_page_counter: bool,
is_carousel: bool,
) {
self.choices = new_choices;
if reset_page_counter {
self.page_counter = 0;
}
self.update(ctx);
self.is_carousel = is_carousel;
}
/// Navigating to the chosen page index.
pub fn set_page_counter(&mut self, ctx: &mut EventCtx, page_counter: u8) {
self.page_counter = page_counter;
self.update(ctx);
}
/// Display current, previous and next choice according to
/// the current ChoiceItem.
fn paint_choices(&mut self) {
// Performing the appropriate `paint_XXX()` for the main choice
// and two adjacent choices when present
// In case of carousel mode, also showing the ones from other end.
self.show_current_choice();
if self.has_previous_choice() {
self.show_previous_choice();
} else if self.is_carousel {
self.show_last_choice_on_left();
}
if self.has_next_choice() {
self.show_next_choice();
} else if self.is_carousel {
self.show_first_choice_on_right();
}
}
/// Setting current buttons, and clearing.
fn update(&mut self, ctx: &mut EventCtx) {
self.set_buttons(ctx);
self.clear(ctx);
}
/// Clearing the whole area and requesting repaint.
fn clear(&mut self, ctx: &mut EventCtx) {
self.pad.clear();
ctx.request_paint();
}
fn last_page_index(&self) -> u8 {
self.choices.count() as u8 - 1
}
pub fn has_previous_choice(&self) -> bool {
self.page_counter > 0
}
pub fn has_next_choice(&self) -> bool {
self.page_counter < self.last_page_index()
}
fn current_choice(&self) -> ChoiceItem {
self.get_choice(self.page_counter)
}
fn get_choice(&self, index: u8) -> ChoiceItem {
self.choices.get(index)
}
fn show_current_choice(&self) {
self.current_choice().paint_center();
}
fn show_previous_choice(&self) {
self.get_choice(self.page_counter - 1).paint_left();
}
fn show_next_choice(&self) {
self.get_choice(self.page_counter + 1).paint_right();
}
fn show_last_choice_on_left(&self) {
self.get_choice(self.last_page_index()).paint_left();
}
fn show_first_choice_on_right(&self) {
self.get_choice(0).paint_right();
}
fn decrease_page_counter(&mut self) {
self.page_counter -= 1;
}
fn increase_page_counter(&mut self) {
self.page_counter += 1;
}
fn page_counter_to_zero(&mut self) {
self.page_counter = 0;
}
fn page_counter_to_max(&mut self) {
self.page_counter = self.last_page_index();
}
pub fn page_index(&self) -> u8 {
self.page_counter
}
/// Updating the visual state of the buttons after each event.
/// All three buttons are handled based upon the current choice.
/// If defined in the current choice, setting their text,
/// whether they are long-pressed, and painting them.
///
/// NOTE: ButtonController is handling the painting, and
/// it will not repaint the buttons unless some of them changed.
fn set_buttons(&mut self, ctx: &mut EventCtx) {
// TODO: offer the possibility to change the buttons from the client
// (button details could be changed in the same index)
// Use-case: BIN button in PIN is deleting last digit if the PIN is not empty,
// otherwise causing Cancel. Would be nice to allow deleting as a single click
// and Cancel as HTC. PIN client would check if the PIN is empty/not and
// adjust the HTC/not.
let btn_layout = self.current_choice().btn_layout();
self.buttons.mutate(ctx, |_ctx, buttons| {
buttons.set(btn_layout);
});
}
}
impl<F> Component for ChoicePage<F>
where
F: ChoiceFactory,
{
type Msg = ChoicePageMsg;
fn place(&mut self, bounds: Rect) -> Rect {
let (content_area, button_area) = bounds.split_bottom(theme::BUTTON_HEIGHT);
self.pad.place(content_area);
self.buttons.place(button_area);
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
let button_event = self.buttons.event(ctx, event);
if let Some(ButtonControllerMsg::Triggered(pos)) = button_event {
match pos {
ButtonPos::Left => {
if self.has_previous_choice() {
// Clicked BACK. Decrease the page counter.
self.decrease_page_counter();
self.update(ctx);
} else if self.is_carousel {
// In case of carousel going to the right end.
self.page_counter_to_max();
self.update(ctx);
} else {
// Triggered LEFTmost button. Send event
self.clear(ctx);
return Some(ChoicePageMsg::LeftMost);
}
}
ButtonPos::Right => {
if self.has_next_choice() {
// Clicked NEXT. Increase the page counter.
self.increase_page_counter();
self.update(ctx);
} else if self.is_carousel {
// In case of carousel going to the left end.
self.page_counter_to_zero();
self.update(ctx);
} else {
// Triggered RIGHTmost button. Send event
self.clear(ctx);
return Some(ChoicePageMsg::RightMost);
}
}
ButtonPos::Middle => {
// Clicked SELECT. Send current choice index
self.clear(ctx);
return Some(ChoicePageMsg::Choice(self.page_counter));
}
}
};
None
}
fn paint(&mut self) {
self.pad.paint();
self.buttons.paint();
self.paint_choices();
}
}
#[cfg(feature = "ui_debug")]
impl<F> crate::trace::Trace for ChoicePage<F>
where
F: ChoiceFactory,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("ChoicePage");
t.kw_pair("active_page", inttostr!(self.page_counter));
t.kw_pair("page_count", inttostr!(self.choices.count() as u8));
t.kw_pair("is_carousel", booltostr!(self.is_carousel));
if self.has_previous_choice() {
t.field("prev_choice", &self.get_choice(self.page_counter - 1));
} else if self.is_carousel {
// In case of carousel going to the left end.
t.field("prev_choice", &self.get_choice(self.last_page_index()));
} else {
t.string("prev_choice");
t.symbol("None");
}
t.field("current_choice", &self.current_choice());
if self.has_next_choice() {
t.field("next_choice", &self.get_choice(self.page_counter + 1));
} else if self.is_carousel {
// In case of carousel going to the very left.
t.field("next_choice", &self.get_choice(0));
} else {
t.string("next_choice");
t.symbol("None");
}
t.field("buttons", &self.buttons);
t.close();
}
}

View File

@ -0,0 +1,338 @@
use crate::ui::{geometry::Point, display::Font, util::char_to_string};
use heapless::String;
use super::{
common::{display, display_center, display_right},
ButtonDetails, ButtonLayout,
};
const MIDDLE_ROW: i16 = 61;
const LEFT_COL: i16 = 1;
const MIDDLE_COL: i16 = 64;
const RIGHT_COL: i16 = 127;
/// Helper to unite the row height.
fn row_height() -> i16 {
// It never reaches the maximum height
Font::NORMAL.line_height() - 4
}
/// Component that can be used as a choice item.
/// Allows to have a choice of anything that can be painted on screen.
///
/// Controls the painting of the current, previous and next item
/// through `paint_XXX()` methods.
/// Defines the behavior of all three buttons through `btn_XXX` attributes.
///
/// Possible implementations:
/// - [x] `TextChoiceItem` - for regular text
/// - [x] `MultilineTextChoiceItem` - for multiline text
/// - [x] `BigCharacterChoiceItem` - for one big character
/// - [ ] `IconChoiceItem` - for showing icons
/// - [ ] `JustCenterChoice` - paint_left() and paint_right() show nothing
/// - [ ] `LongStringsChoice` - paint_left() and paint_right() show ellipsis
pub trait ChoiceItemAPI {
fn paint_center(&mut self);
fn paint_left(&mut self);
fn paint_right(&mut self);
fn btn_layout(&self) -> ButtonLayout<&'static str>;
}
// TODO: consider having
// pub trait ChoiceItemOperations {}
// TODO: consider storing all the text components as `T: AsRef<str>`
// Tried, but it makes the code unnecessarily messy with all the <T>
// definitions, which needs to be added to all the components using it.
/// Storing all the possible implementations of `ChoiceItemAPI`.
/// Done like this as we want to use multiple different choice pages
/// at the same time in `ChoicePage` - for example Multiline and BigLetters
#[derive(Clone)]
pub enum ChoiceItem {
Text(TextChoiceItem),
MultilineText(MultilineTextChoiceItem),
BigCharacter(BigCharacterChoiceItem),
}
impl ChoiceItem {
// TODO: can we somehow avoid the repetitions here?
pub fn set_left_btn(&mut self, btn_left: Option<ButtonDetails<&'static str>>) {
match self {
ChoiceItem::Text(item) => item.btn_layout.btn_left = btn_left,
ChoiceItem::MultilineText(item) => item.btn_layout.btn_left = btn_left,
ChoiceItem::BigCharacter(item) => item.btn_layout.btn_left = btn_left,
}
}
pub fn set_middle_btn(&mut self, btn_middle: Option<ButtonDetails<&'static str>>) {
match self {
ChoiceItem::Text(item) => item.btn_layout.btn_middle = btn_middle,
ChoiceItem::MultilineText(item) => item.btn_layout.btn_middle = btn_middle,
ChoiceItem::BigCharacter(item) => item.btn_layout.btn_middle = btn_middle,
}
}
pub fn set_right_btn(&mut self, btn_right: Option<ButtonDetails<&'static str>>) {
match self {
ChoiceItem::Text(item) => item.btn_layout.btn_right = btn_right,
ChoiceItem::MultilineText(item) => item.btn_layout.btn_right = btn_right,
ChoiceItem::BigCharacter(item) => item.btn_layout.btn_right = btn_right,
}
}
pub fn set_text(&mut self, text: String<50>) {
match self {
ChoiceItem::Text(item) => item.text = text,
ChoiceItem::MultilineText(item) => item.text = text,
ChoiceItem::BigCharacter(_) => {
panic!("No text setting for BigCharacter")
}
}
}
}
impl ChoiceItemAPI for ChoiceItem {
fn paint_center(&mut self) {
match self {
ChoiceItem::Text(item) => item.paint_center(),
ChoiceItem::MultilineText(item) => item.paint_center(),
ChoiceItem::BigCharacter(item) => item.paint_center(),
}
}
fn paint_left(&mut self) {
match self {
ChoiceItem::Text(item) => item.paint_left(),
ChoiceItem::MultilineText(item) => item.paint_left(),
ChoiceItem::BigCharacter(item) => item.paint_left(),
}
}
fn paint_right(&mut self) {
match self {
ChoiceItem::Text(item) => item.paint_right(),
ChoiceItem::MultilineText(item) => item.paint_right(),
ChoiceItem::BigCharacter(item) => item.paint_right(),
}
}
fn btn_layout(&self) -> ButtonLayout<&'static str> {
match self {
ChoiceItem::Text(item) => item.btn_layout(),
ChoiceItem::MultilineText(item) => item.btn_layout(),
ChoiceItem::BigCharacter(item) => item.btn_layout(),
}
}
}
/// Simple string component used as a choice item.
#[derive(Clone)]
pub struct TextChoiceItem {
pub text: String<50>,
pub btn_layout: ButtonLayout<&'static str>,
}
impl TextChoiceItem {
pub fn new<T>(text: T, btn_layout: ButtonLayout<&'static str>) -> Self
where
T: AsRef<str>,
{
Self {
text: String::from(text.as_ref()),
btn_layout,
}
}
}
impl ChoiceItemAPI for TextChoiceItem {
fn paint_center(&mut self) {
// Displaying the center choice lower than the rest,
// to make it more clear this is the current choice
// (and also the left and right ones do not collide with it)
display_center(
Point::new(MIDDLE_COL, MIDDLE_ROW + row_height()),
&self.text,
Font::NORMAL,
);
}
fn paint_left(&mut self) {
display(
Point::new(LEFT_COL, MIDDLE_ROW),
&self.text,
Font::NORMAL,
);
}
fn paint_right(&mut self) {
display_right(
Point::new(RIGHT_COL, MIDDLE_ROW),
&self.text,
Font::NORMAL,
);
}
fn btn_layout(&self) -> ButtonLayout<&'static str> {
self.btn_layout.clone()
}
}
/// Multiline string component used as a choice item.
///
/// Lines are delimited by '\n' character, unless specified explicitly.
#[derive(Clone)]
pub struct MultilineTextChoiceItem {
// Arbitrary chosen. TODO: agree on this
pub text: String<50>,
delimiter: char,
pub btn_layout: ButtonLayout<&'static str>,
}
impl MultilineTextChoiceItem {
pub fn new(text: String<50>, btn_layout: ButtonLayout<&'static str>) -> Self {
Self {
text,
delimiter: '\n',
btn_layout,
}
}
/// Allows for changing the line delimiter to arbitrary char.
pub fn use_delimiter(mut self, delimiter: char) -> Self {
self.delimiter = delimiter;
self
}
}
// TODO: Make all the text be centered vertically - account for amount of lines.
impl ChoiceItemAPI for MultilineTextChoiceItem {
fn paint_center(&mut self) {
// Displaying the center choice lower than the rest,
// to make it more clear this is the current choice
for (index, line) in self.text.split(self.delimiter).enumerate() {
let offset = MIDDLE_ROW + index as i16 * row_height() + row_height();
display_center(Point::new(MIDDLE_COL, offset), &line, Font::NORMAL);
}
}
fn paint_left(&mut self) {
for (index, line) in self.text.split(self.delimiter).enumerate() {
let offset = MIDDLE_ROW + index as i16 * row_height();
display(Point::new(LEFT_COL, offset), &line, Font::NORMAL);
}
}
fn paint_right(&mut self) {
for (index, line) in self.text.split(self.delimiter).enumerate() {
let offset = MIDDLE_ROW + index as i16 * row_height();
display_right(Point::new(RIGHT_COL, offset), &line, Font::NORMAL);
}
}
fn btn_layout(&self) -> ButtonLayout<&'static str> {
self.btn_layout.clone()
}
}
/// Choice item displaying single characters in BIG font.
/// Middle choice is magnified 4 times, left and right 2 times.
#[derive(Clone)]
pub struct BigCharacterChoiceItem {
pub ch: char,
pub btn_layout: ButtonLayout<&'static str>,
}
impl BigCharacterChoiceItem {
pub fn new(ch: char, btn_layout: ButtonLayout<&'static str>) -> Self {
Self { ch, btn_layout }
}
/// Taking the first character from the `text`.
pub fn from_str<T>(text: T, btn_layout: ButtonLayout<&'static str>) -> Self
where
T: AsRef<str>,
{
Self {
ch: text.as_ref().chars().next().unwrap(),
btn_layout,
}
}
fn _paint_char(&mut self, baseline: Point) {
display(
baseline,
&char_to_string::<1>(self.ch),
Font::NORMAL,
);
}
}
impl ChoiceItemAPI for BigCharacterChoiceItem {
fn paint_center(&mut self) {
self._paint_char(Point::new(MIDDLE_COL - 12, MIDDLE_ROW + 9));
}
fn paint_left(&mut self) {
self._paint_char(Point::new(LEFT_COL, MIDDLE_ROW));
}
fn paint_right(&mut self) {
self._paint_char(Point::new(RIGHT_COL - 12, MIDDLE_ROW));
}
fn btn_layout(&self) -> ButtonLayout<&'static str> {
self.btn_layout.clone()
}
}
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for ChoiceItem {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("ChoiceItem");
match self {
ChoiceItem::Text(item) => item.trace(t),
ChoiceItem::MultilineText(item) => item.trace(t),
ChoiceItem::BigCharacter(item) => item.trace(t),
}
t.close();
}
}
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for TextChoiceItem {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("TextChoiceItem");
t.content_flag();
t.string(&self.text);
t.content_flag();
t.close();
}
}
#[cfg(feature = "ui_debug")]
use crate::ui::util;
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for MultilineTextChoiceItem {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("MultilineTextChoiceItem");
t.content_flag();
t.string(&self.text);
t.content_flag();
t.field("delimiter", &(util::char_to_string::<1>(self.delimiter)));
t.close();
}
}
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for BigCharacterChoiceItem {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("BigCharacterChoiceItem");
t.content_flag();
t.string(&util::char_to_string::<1>(self.ch));
t.content_flag();
t.close();
}
}

View File

@ -0,0 +1,120 @@
use crate::ui::{
display::{self, Font, Icon},
geometry::{Offset, Point},
model_tr::constant,
};
use heapless::String;
use super::theme;
/// Display header text.
pub fn display_header<T: AsRef<str>>(baseline: Point, text: T) {
// TODO: make this centered?
display::text(
baseline,
text.as_ref(),
theme::FONT_HEADER,
theme::FG,
theme::BG,
);
}
/// Display bold white text on black background
pub fn display<T: AsRef<str>>(baseline: Point, text: &T, font: Font) {
display::text(baseline, text.as_ref(), font, theme::FG, theme::BG);
}
/// Display white text on black background,
/// centered around a baseline Point
pub fn display_center<T: AsRef<str>>(baseline: Point, text: &T, font: Font) {
display::text_center(baseline, text.as_ref(), font, theme::FG, theme::BG);
}
/// Display white text on black background,
/// with right boundary at a baseline Point
pub fn display_right<T: AsRef<str>>(baseline: Point, text: &T, font: Font) {
display::text_right(baseline, text.as_ref(), font, theme::FG, theme::BG);
}
const MAX_VISIBLE_CHARS: usize = 18;
const TOP_ROW_TEXT: i16 = 7;
/// Display indication of current user input - PIN, passphrase etc.
/// Showing one asterisk for each character of the input.
pub fn display_dots_center_top(dots_amount: usize, offset_from_top: i16) {
let y_position = TOP_ROW_TEXT + offset_from_top;
let dots_visible = dots_amount.min(MAX_VISIBLE_CHARS);
// String::repeat() is not available for heapless::String
let mut dots: String<MAX_VISIBLE_CHARS> = String::new();
for _ in 0..dots_visible {
dots.push_str("*").unwrap();
}
// Giving some notion of change even for longer-than-visible passphrases
// - slightly shifting the dots to the left and right after each new digit
if dots_amount > MAX_VISIBLE_CHARS && dots_amount % 2 == 0 {
display_center(Point::new(61, y_position), &dots, Font::MONO);
} else {
display_center(Point::new(64, y_position), &dots, Font::MONO);
}
}
/// Display secret input that user is currently doing - PIN, passphrase etc.
pub fn display_secret_center_top<T: AsRef<str>>(secret: T, offset_from_top: i16) {
let y_position = TOP_ROW_TEXT + offset_from_top;
let char_amount = secret.as_ref().len();
if char_amount <= MAX_VISIBLE_CHARS {
display_center(Point::new(64, y_position), &secret, Font::MONO);
} else {
// Show the last part with preceding ellipsis to show something is hidden
let ellipsis = "...";
let offset: usize = char_amount.saturating_sub(MAX_VISIBLE_CHARS) + ellipsis.len();
let to_show = build_string!(MAX_VISIBLE_CHARS, ellipsis, &secret.as_ref()[offset..]);
display_center(Point::new(64, y_position), &to_show, Font::MONO);
}
}
/// Display title and possible subtitle together with a dotted line spanning
/// the entire width.
/// Returning the painted height of the whole header.
pub fn paint_header<T: AsRef<str>>(top_left: Point, title: T, subtitle: Option<T>) -> i16 {
let text_heigth = theme::FONT_HEADER.text_height();
let title_baseline = top_left + Offset::y(text_heigth);
display_header(title_baseline, title);
// Optionally painting the subtitle as well
// (and offsetting the dotted line in that case)
let mut dotted_line_offset = text_heigth + 2;
if let Some(subtitle) = subtitle {
dotted_line_offset += text_heigth;
display_header(title_baseline + Offset::y(text_heigth), subtitle);
}
let line_start = top_left + Offset::y(dotted_line_offset);
display::dotted_line_horizontal(line_start, constant::WIDTH, theme::FG, 2);
dotted_line_offset
}
/// Draws icon and text on the same line - icon on the left.
pub fn icon_with_text<T: AsRef<str>>(baseline: Point, icon: Icon, text: T, font: Font) {
icon.draw_bottom_left(baseline, theme::FG, theme::BG);
let text_x_offset = icon.width() + 2;
display(baseline + Offset::x(text_x_offset), &text.as_ref(), font);
}
/// Draw two lines - icon with label text (key) and another text (value) below.
/// Returns the height painted below the given baseline.
pub fn key_value_icon<T: AsRef<str>>(
baseline: Point,
icon: Icon,
label: T,
label_font: Font,
value: T,
value_font: Font,
) -> i16 {
icon_with_text(baseline, icon, label, label_font);
let line_height = value_font.line_height();
let next_line = baseline + Offset::y(line_height);
display(next_line, &value, value_font);
line_height
}

View File

@ -1,9 +1,10 @@
use crate::{
time::Instant,
time::{Duration, Instant},
ui::{
component::{Component, Event, EventCtx},
display::Icon,
event::ButtonEvent,
geometry::{Point, Rect},
geometry::Rect,
model_tr::component::{loader::Loader, ButtonPos, LoaderMsg, LoaderStyleSheet},
},
};
@ -13,36 +14,74 @@ pub enum HoldToConfirmMsg {
FailedToConfirm,
}
pub struct HoldToConfirm {
pub struct HoldToConfirm<T> {
area: Rect,
pos: ButtonPos,
loader: Loader,
baseline: Point,
loader: Loader<T>,
text_width: i16,
}
impl HoldToConfirm {
pub fn new(pos: ButtonPos, text: &'static str, styles: LoaderStyleSheet) -> Self {
// TODO: support icons
// TODO: could have some icon signaling "HOLD", so that we do not need
// to write `HOLD TO CONFIRM` (which hardly fits the screen), but just `{icon}
// CONFIRM` TODO: could unite with `Button` so we can use the same features for
// both
impl<T: AsRef<str>> HoldToConfirm<T> {
pub fn text(pos: ButtonPos, text: T, styles: LoaderStyleSheet, duration: Duration) -> Self {
let text_width = styles.normal.font.text_width(text.as_ref());
Self {
area: Rect::zero(),
pos,
loader: Loader::new(text, styles),
baseline: Point::zero(),
loader: Loader::text(text, styles).with_growing_duration(duration),
text_width,
}
}
pub fn icon(pos: ButtonPos, icon: Icon, styles: LoaderStyleSheet, duration: Duration) -> Self {
let text_width = icon.width() as i16;
Self {
area: Rect::zero(),
pos,
loader: Loader::icon(icon, styles).with_growing_duration(duration),
text_width,
}
}
/// Updating the text of the component and re-placing it.
pub fn set_text(&mut self, text: T, button_area: Rect) {
self.text_width = self.loader.get_text_width(&text) as i16;
self.loader.set_text(text);
self.place(button_area);
}
pub fn reset(&mut self) {
self.loader.reset();
}
pub fn set_duration(&mut self, duration: Duration) {
self.loader.set_duration(duration);
}
pub fn get_duration(&self) -> Duration {
self.loader.get_duration()
}
pub fn get_text(&self) -> &T {
self.loader.get_text()
}
fn placement(&mut self, area: Rect, pos: ButtonPos) -> Rect {
let button_width = self.text_width + 7;
match pos {
ButtonPos::Left => area.split_left(button_width).0,
ButtonPos::Right => area.split_right(button_width).1,
ButtonPos::Middle => area.split_center(button_width).1,
}
}
}
impl Component for HoldToConfirm {
impl<T: AsRef<str>> Component for HoldToConfirm<T> {
type Msg = HoldToConfirmMsg;
fn place(&mut self, bounds: Rect) -> Rect {
@ -52,10 +91,10 @@ impl Component for HoldToConfirm {
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
match event {
Event::Button(ButtonEvent::ButtonPressed(which)) if self.pos.hit(&which) => {
Event::Button(ButtonEvent::HoldStarted) => {
self.loader.start_growing(ctx, Instant::now());
}
Event::Button(ButtonEvent::ButtonReleased(which)) if self.pos.hit(&which) => {
Event::Button(ButtonEvent::HoldEnded) => {
if self.loader.is_animating() {
self.loader.start_shrinking(ctx, Instant::now());
}
@ -81,7 +120,7 @@ impl Component for HoldToConfirm {
}
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for HoldToConfirm {
impl<T: AsRef<str>> crate::trace::Trace for HoldToConfirm<T> {
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
d.open("HoldToConfirm");
self.loader.trace(d);

View File

@ -1,8 +1,7 @@
use super::button::{Button, ButtonMsg::Clicked};
use crate::ui::{
component::{Child, Component, Event, EventCtx},
display::Font,
geometry::Rect,
geometry::Rect, model_tr::theme,
};
pub enum DialogMsg<T> {
@ -43,7 +42,7 @@ where
type Msg = DialogMsg<T::Msg>;
fn place(&mut self, bounds: Rect) -> Rect {
let button_height = Font::BOLD.line_height() + 2;
let button_height = theme::FONT_BUTTON.line_height() + 2;
let (content_area, button_area) = bounds.split_bottom(button_height);
self.content.place(content_area);
self.left_btn.as_mut().map(|b| b.place(button_area));

View File

@ -0,0 +1,255 @@
use crate::{
micropython::buffer::StrBuffer,
ui::{
component::{Child, Component, Event, EventCtx, Pad},
geometry::{Point, Rect},
},
};
use super::{
common, theme, ButtonAction, ButtonController, ButtonControllerMsg, ButtonLayout, ButtonPos,
FlowPages, Page,
};
/// To be returned directly from Flow.
pub enum FlowMsg {
Confirmed,
Cancelled,
}
// TODO: consider each FlowPage having the ability
// to handle custom actions triggered by some btn.
pub struct Flow<F, const M: usize> {
pages: FlowPages<F, M>,
current_page: Page<M>,
common_title: Option<StrBuffer>,
content_area: Rect,
pad: Pad,
buttons: Child<ButtonController<&'static str>>,
page_counter: u8,
}
impl<F, const M: usize> Flow<F, M>
where
F: Fn(u8) -> Page<M>,
{
pub fn new(pages: FlowPages<F, M>) -> Self {
let current_page = pages.get(0);
Self {
pages,
current_page,
common_title: None,
content_area: Rect::zero(),
pad: Pad::with_background(theme::BG),
// Setting empty layout for now, we do not yet know how many sub-pages the first page
// has. Initial button layout will be set in `place()` after we can call
// `content.page_count()`.
buttons: Child::new(ButtonController::new(ButtonLayout::empty())),
page_counter: 0,
}
}
/// Adding a common title to all pages. The title will not be colliding
/// with the page content, as the content will be offset.
pub fn with_common_title(mut self, title: StrBuffer) -> Self {
self.common_title = Some(title);
self
}
/// Placing current page, setting current buttons and clearing.
fn update(&mut self, ctx: &mut EventCtx, get_new_page: bool) {
if get_new_page {
self.current_page = self.pages.get(self.page_counter);
}
let content_area = self.content_area;
self.current_page.place(content_area);
self.set_buttons(ctx);
self.clear(ctx);
}
/// Clearing the whole area and requesting repaint.
fn clear(&mut self, ctx: &mut EventCtx) {
self.pad.clear();
ctx.request_paint();
}
/// Going to the previous page.
fn go_to_prev_page(&mut self, ctx: &mut EventCtx) {
self.page_counter -= 1;
self.update(ctx, true);
}
/// Going to the next page.
fn go_to_next_page(&mut self, ctx: &mut EventCtx) {
self.page_counter += 1;
self.update(ctx, true);
}
/// Going to page by its absolute index.
/// Negative index means counting from the end.
fn go_to_page_absolute(&mut self, index: i16, ctx: &mut EventCtx) {
if index < 0 {
self.page_counter = (self.pages.count() as i16 + index) as u8;
} else {
self.page_counter = index as u8;
}
self.update(ctx, true);
}
/// Jumping to another page relative to the current one.
fn go_to_page_relative(&mut self, jump: i16, ctx: &mut EventCtx) {
self.page_counter = (self.page_counter as i16 + jump) as u8;
self.update(ctx, true);
}
/// Updating the visual state of the buttons after each event.
/// All three buttons are handled based upon the current choice.
/// If defined in the current choice, setting their text,
/// whether they are long-pressed, and painting them.
///
/// NOTE: ButtonController is handling the painting, and
/// it will not repaint the buttons unless some of them changed.
fn set_buttons(&mut self, ctx: &mut EventCtx) {
let btn_layout = self.current_page.btn_layout();
self.buttons.mutate(ctx, |_ctx, buttons| {
buttons.set(btn_layout);
});
}
/// When current choice contains paginated content, it may use the button
/// event to just paginate itself.
fn event_consumed_by_current_choice(&mut self, ctx: &mut EventCtx, pos: ButtonPos) -> bool {
if matches!(pos, ButtonPos::Left) && self.current_page.has_prev_page() {
self.current_page.go_to_prev_page();
self.update(ctx, false);
true
} else if matches!(pos, ButtonPos::Right) && self.current_page.has_next_page() {
self.current_page.go_to_next_page();
self.update(ctx, false);
true
} else {
false
}
}
}
impl<F, const M: usize> Component for Flow<F, M>
where
F: Fn(u8) -> Page<M>,
{
type Msg = FlowMsg;
fn place(&mut self, bounds: Rect) -> Rect {
let (title_content_area, button_area) = bounds.split_bottom(theme::BUTTON_HEIGHT);
// Accounting for possible title
let content_area = if self.common_title.is_some() {
title_content_area.split_top(10).1
} else {
title_content_area
};
self.content_area = content_area;
// We finally found how long is the first page, and can set its button layout.
self.current_page.place(content_area);
self.buttons = Child::new(ButtonController::new(self.current_page.btn_layout()));
self.pad.place(title_content_area);
self.buttons.place(button_area);
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
let button_event = self.buttons.event(ctx, event);
// Do something when a button was triggered
// and we have some action connected with it
if let Some(ButtonControllerMsg::Triggered(pos)) = button_event {
// When there is a previous or next screen in the current flow,
// handle that first and in case it triggers, then do not continue
if self.event_consumed_by_current_choice(ctx, pos) {
return None;
}
let actions = self.current_page.btn_actions();
let action = actions.get_action(pos);
if let Some(action) = action {
match action {
ButtonAction::PrevPage => {
self.go_to_prev_page(ctx);
return None;
}
ButtonAction::NextPage => {
self.go_to_next_page(ctx);
return None;
}
ButtonAction::GoToIndex(index) => {
self.go_to_page_absolute(index, ctx);
return None;
}
ButtonAction::MovePageRelative(jump) => {
self.go_to_page_relative(jump, ctx);
return None;
}
ButtonAction::Cancel => return Some(FlowMsg::Cancelled),
ButtonAction::Confirm => return Some(FlowMsg::Confirmed),
ButtonAction::Select => {}
ButtonAction::Action(_) => {}
}
}
};
None
}
fn paint(&mut self) {
// TODO: might put horizontal scrollbar at the top right
self.pad.paint();
self.buttons.paint();
if let Some(title) = &self.common_title {
common::paint_header(Point::zero(), title, None);
}
self.current_page.paint();
}
}
#[cfg(feature = "ui_debug")]
use heapless::String;
#[cfg(feature = "ui_debug")]
impl<F, const M: usize> crate::trace::Trace for Flow<F, M>
where
F: Fn(u8) -> Page<M>,
{
/// Accounting for the possibility that button is connected with the
/// currently paginated flow_page (only Prev or Next in that case).
fn get_btn_action(&self, pos: ButtonPos) -> String<25> {
if matches!(pos, ButtonPos::Left) && self.current_page.has_prev_page() {
ButtonAction::PrevPage.string()
} else if matches!(pos, ButtonPos::Right) && self.current_page.has_next_page() {
ButtonAction::NextPage.string()
} else {
let btn_actions = self.current_page.btn_actions();
match btn_actions.get_action(pos) {
Some(action) => action.string(),
None => ButtonAction::empty(),
}
}
}
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("Flow");
t.kw_pair("flow_page", inttostr!(self.page_counter));
t.kw_pair("flow_page_count", inttostr!(self.pages.count()));
self.report_btn_actions(t);
if let Some(title) = &self.common_title {
t.title(title.as_ref());
}
t.field("content_area", &self.content_area);
t.field("buttons", &self.buttons);
t.field("flow_page", &self.current_page);
t.close()
}
}

View File

@ -0,0 +1,309 @@
use crate::{
micropython::{buffer::StrBuffer},
ui::{
component::Paginate,
display::{Font, Icon, IconAndName},
geometry::{Offset, Rect},
model_tr::theme,
util::ResultExt
},
};
use heapless::Vec;
use super::{
flow_pages_poc_helpers::{
LayoutFit, LayoutSink, LineAlignment, Op, TextLayout, TextNoOp, TextRenderer, TextStyle,
ToDisplay,
},
ButtonActions, ButtonDetails, ButtonLayout,
};
/// Holding specific workflows that are created in `layout.rs`.
/// Is returning a `Page` (page/screen) on demand
/// based on the current page in `Flow`.
/// Before, when `layout.rs` was defining a `heapless::Vec` of `Page`s,
/// it was a very stack-expensive operation and StackOverflow was encountered.
/// With this "lazy-loading" approach (creating each page on demand) we can
/// have theoretically unlimited number of pages without triggering SO.
/// (Currently only the current page is stored on stack - in
/// `Flow::current_page`.)
pub struct FlowPages<F, const M: usize> {
/// Function/closure that will return appropriate page on demand.
get_page: F,
/// Number of pages in the flow.
page_count: u8,
}
impl<F, const M: usize> FlowPages<F, M>
where
F: Fn(u8) -> Page<M>,
{
pub fn new(get_page: F, page_count: u8) -> Self {
Self {
get_page,
page_count,
}
}
pub fn get(&self, page_index: u8) -> Page<M> {
(self.get_page)(page_index)
}
pub fn count(&self) -> u8 {
self.page_count
}
}
#[derive(Clone)]
pub struct Page<const M: usize> {
ops: Vec<Op, M>,
layout: TextLayout,
btn_layout: ButtonLayout<&'static str>,
btn_actions: ButtonActions,
current_page: usize,
page_count: usize,
char_offset: usize,
}
// For `layout.rs`
impl<const M: usize> Page<M> {
pub fn new(btn_layout: ButtonLayout<&'static str>, btn_actions: ButtonActions) -> Self {
let style = TextStyle::new(
Font::NORMAL,
theme::FG,
theme::BG,
theme::FG,
theme::FG,
);
Self {
ops: Vec::new(),
layout: TextLayout::new(style),
btn_layout,
btn_actions,
current_page: 0,
page_count: 1,
char_offset: 0,
}
}
}
// For `flow.rs`
impl<const M: usize> Page<M> {
pub fn paint(&mut self) {
self.change_page(self.current_page);
self.layout_content(&mut TextRenderer);
}
pub fn btn_layout(&self) -> ButtonLayout<&'static str> {
// When we are in pagination inside this flow,
// show the up and down arrows on appropriate sides
let current = self.btn_layout.clone();
let btn_left = if self.has_prev_page() {
Some(ButtonDetails::up_arrow_icon_wide())
} else {
current.btn_left
};
let btn_right = if self.has_next_page() {
Some(ButtonDetails::down_arrow_icon_wide())
} else {
current.btn_right
};
ButtonLayout::new(btn_left, current.btn_middle, btn_right)
}
pub fn place(&mut self, bounds: Rect) -> Rect {
self.layout.bounds = bounds;
self.page_count = self.page_count();
bounds
}
pub fn btn_actions(&self) -> ButtonActions {
self.btn_actions.clone()
}
pub fn has_prev_page(&self) -> bool {
self.current_page > 0
}
pub fn has_next_page(&self) -> bool {
self.current_page < self.page_count - 1
}
pub fn go_to_prev_page(&mut self) {
self.current_page -= 1;
}
pub fn go_to_next_page(&mut self) {
self.current_page += 1;
}
}
// For `layout.rs` - single operations
impl<const M: usize> Page<M> {
pub fn with_new_item(mut self, item: Op) -> Self {
self.ops
.push(item)
.assert_if_debugging_ui("Could not push to self.ops");
self
}
pub fn text(self, text: StrBuffer) -> Self {
self.with_new_item(Op::Text(ToDisplay::new(text)))
}
pub fn newline(self) -> Self {
self.with_new_item(Op::Text(ToDisplay::new("\n".into())))
}
pub fn newline_half(self) -> Self {
self.with_new_item(Op::Text(ToDisplay::new("\r".into())))
}
pub fn next_page(self) -> Self {
self.with_new_item(Op::NextPage)
}
pub fn icon(self, icon: IconAndName) -> Self {
self.with_new_item(Op::Icon(Icon::new(icon)))
}
pub fn font(self, font: Font) -> Self {
self.with_new_item(Op::Font(font))
}
pub fn offset(self, offset: Offset) -> Self {
self.with_new_item(Op::CursorOffset(offset))
}
pub fn alignment(self, alignment: LineAlignment) -> Self {
self.with_new_item(Op::LineAlignment(alignment))
}
}
// For `layout.rs` - aggregating operations
impl<const M: usize> Page<M> {
pub fn icon_label_text(self, icon: IconAndName, label: StrBuffer, text: StrBuffer) -> Self {
self.icon_with_offset(icon, 3)
.text_normal(label)
.newline()
.text_bold(text)
}
pub fn icon_with_offset(self, icon: IconAndName, x_offset: i16) -> Self {
self.icon(icon).offset(Offset::x(x_offset))
}
pub fn text_normal(self, text: StrBuffer) -> Self {
self.font(Font::NORMAL).text(text)
}
pub fn text_bold(self, text: StrBuffer) -> Self {
self.font(Font::BOLD).text(text)
}
}
// For painting and pagination
impl<const M: usize> Page<M> {
pub fn set_char_offset(&mut self, char_offset: usize) {
self.char_offset = char_offset;
}
pub fn layout_content(&self, sink: &mut dyn LayoutSink) -> LayoutFit {
let mut cursor = self.layout.initial_cursor();
self.layout
.layout_ops(self.ops.clone(), &mut cursor, self.char_offset, sink)
}
}
// Pagination
impl<const M: usize> Paginate for Page<M> {
fn page_count(&mut self) -> usize {
let mut page_count = 1; // There's always at least one page.
let mut char_offset = 0;
// Make sure we're starting from the beginning.
self.set_char_offset(char_offset);
// Looping through the content and counting pages
// until we finally fit.
loop {
let fit = self.layout_content(&mut TextNoOp);
match fit {
LayoutFit::Fitting { .. } => {
break; // TODO: We should consider if there's more content
// to render.
}
LayoutFit::OutOfBounds {
processed_chars, ..
} => {
page_count += 1;
char_offset += processed_chars;
self.set_char_offset(char_offset);
}
}
}
// Reset the char offset back to the beginning.
self.set_char_offset(0);
page_count
}
fn change_page(&mut self, to_page: usize) {
let mut active_page = 0;
let mut char_offset = 0;
// Make sure we're starting from the beginning.
self.set_char_offset(char_offset);
// Looping through the content until we arrive at
// the wanted page.
while active_page < to_page {
let fit = self.layout_content(&mut TextNoOp);
match fit {
LayoutFit::Fitting { .. } => {
break; // TODO: We should consider if there's more content
// to render.
}
LayoutFit::OutOfBounds {
processed_chars, ..
} => {
active_page += 1;
char_offset += processed_chars;
self.set_char_offset(char_offset);
}
}
}
}
}
#[cfg(feature = "ui_debug")]
pub mod trace {
use crate::ui::model_tr::component::flow_pages_poc_helpers::TraceSink;
use super::*;
pub struct TraceText<'a, const M: usize>(pub &'a Page<M>);
impl<'a, const M: usize> crate::trace::Trace for TraceText<'a, M> {
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
d.content_flag();
self.0.layout_content(&mut TraceSink(d));
d.content_flag();
}
}
}
#[cfg(feature = "ui_debug")]
impl<const M: usize> crate::trace::Trace for Page<M> {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("Page");
t.kw_pair("active_page", inttostr!(self.current_page as u8));
t.kw_pair("page_count", inttostr!(self.page_count as u8));
t.field("content", &trace::TraceText(self));
t.close();
}
}

View File

@ -0,0 +1,716 @@
//! Mostly copy-pasted stuff from ui/component/text,
//! but with small modifications.
//! It is really mostly changing Op::Text(&'a str) to Op::Text(String<100>),
//! having self.ops as Vec<Op, 30> and changes revolving around it.
//! Even if some stuff could be reused now, I copy-pasted it anyway, as this
//! extension for Icons, Offsets, etc. should no longer live in
//! ui/component/text, and so they can be freely removed (as they are here as
//! well).
use crate::{
micropython::buffer::StrBuffer,
ui::{
display::{self, Color, Font, Icon},
geometry::{Offset, Point, Rect},
},
};
use heapless::Vec;
// TODO: consider moving into T: AsRef<str> instead if StrBuffer?
#[derive(Clone)]
pub struct ToDisplay {
pub text: StrBuffer,
pub length: usize,
}
impl ToDisplay {
pub fn new(text: StrBuffer) -> Self {
Self {
text: text.clone(),
length: text.len(),
}
}
}
/// Operations that can be done on FormattedText.
#[derive(Clone)]
pub enum Op {
/// Render text with current color and font.
Text(ToDisplay),
/// Render icon.
Icon(Icon),
/// Set current text color.
Color(Color),
/// Set currently used font.
Font(Font),
/// Set currently used line alignment.
LineAlignment(LineAlignment),
/// Move the current cursor by specified Offset.
CursorOffset(Offset),
/// Force continuing on the next page.
NextPage,
}
#[derive(Copy, Clone)]
pub enum LineBreaking {
/// Break line only at whitespace, if possible. If we don't find any
/// whitespace, break words.
BreakAtWhitespace,
/// Break words, adding a hyphen before the line-break. Does not use any
/// smart algorithm, just char-by-char.
BreakWordsAndInsertHyphen,
}
#[derive(Copy, Clone)]
pub enum PageBreaking {
/// Stop after hitting the bottom-right edge of the bounds.
Cut,
/// Before stopping at the bottom-right edge, insert ellipsis to signify
/// more content is available, but only if no hyphen has been inserted yet.
CutAndInsertEllipsis,
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum LineAlignment {
Left,
Center,
Right,
}
/// Visual instructions for laying out a formatted block of text.
#[derive(Copy, Clone)]
pub struct TextLayout {
/// Bounding box restricting the layout dimensions.
pub bounds: Rect,
/// Additional space before beginning of text, can be negative to shift text
/// upwards.
pub padding_top: i16,
/// Additional space between end of text and bottom of bounding box, can be
/// negative.
pub padding_bottom: i16,
/// Fonts, colors, line/page breaking behavior.
pub style: TextStyle,
}
#[derive(Copy, Clone)]
pub struct TextStyle {
/// Text font ID. Can be overridden by `Op::Font`.
pub text_font: Font,
/// Text color. Can be overridden by `Op::Color`.
pub text_color: Color,
/// Background color.
pub background_color: Color,
/// Foreground color used for drawing the hyphen.
pub hyphen_color: Color,
/// Foreground color used for drawing the ellipsis.
pub ellipsis_color: Color,
/// Specifies which line-breaking strategy to use.
pub line_breaking: LineBreaking,
/// Specifies what to do at the end of the page.
pub page_breaking: PageBreaking,
/// Specifies how to align text on the line.
pub line_alignment: LineAlignment,
}
impl TextStyle {
pub const fn new(
text_font: Font,
text_color: Color,
background_color: Color,
hyphen_color: Color,
ellipsis_color: Color,
) -> Self {
TextStyle {
text_font,
text_color,
background_color,
hyphen_color,
ellipsis_color,
line_breaking: LineBreaking::BreakAtWhitespace,
page_breaking: PageBreaking::CutAndInsertEllipsis,
line_alignment: LineAlignment::Left,
}
}
}
impl TextLayout {
/// Create a new text layout, with empty size and default text parameters
/// filled from `T`.
pub fn new(style: TextStyle) -> Self {
Self {
bounds: Rect::zero(),
padding_top: 0,
padding_bottom: 0,
style,
}
}
pub fn with_bounds(mut self, bounds: Rect) -> Self {
self.bounds = bounds;
self
}
/// Baseline `Point` where we are starting to draw the text.
pub fn initial_cursor(&self) -> Point {
self.bounds.top_left() + Offset::y(self.style.text_font.text_height() + self.padding_top)
}
/// Trying to fit the content on the current screen.
pub fn fit_text(&self, text: &str) -> LayoutFit {
self.layout_text(text, &mut self.initial_cursor(), &mut TextNoOp)
}
/// Draw as much text as possible on the current screen.
pub fn render_text(&self, text: &str) {
self.layout_text(text, &mut self.initial_cursor(), &mut TextRenderer);
}
/// Y coordinate of the bottom of the available space/bounds
pub fn bottom_y(&self) -> i16 {
(self.bounds.y1 - self.padding_bottom).max(self.bounds.y0)
}
/// X coordinate of the right of the available space/bounds
pub fn right_x(&self) -> i16 {
self.bounds.x1
}
/// Perform some operations defined on `Op` for a list of those `Op`s
/// - e.g. changing the color, changing the font or rendering the text.
pub fn layout_ops<const M: usize>(
mut self,
ops: Vec<Op, M>,
cursor: &mut Point,
skip_bytes: usize,
sink: &mut dyn LayoutSink,
) -> LayoutFit {
let init_cursor = *cursor;
let mut total_processed_chars = 0;
let mut skipped = 0;
for op in ops {
let real_op = {
match op {
Op::Text(to_display) if skipped < skip_bytes => {
skipped = skipped.saturating_add(to_display.length);
if skipped > skip_bytes {
let leave_bytes = skipped - skip_bytes;
let new_display = ToDisplay {
text: to_display.text,
length: leave_bytes,
};
Some(Op::Text(new_display))
} else {
None
}
}
Op::Icon(_) if skipped < skip_bytes => {
// Assume the icon accounts for one character
skipped = skipped.saturating_add(1);
None
}
Op::NextPage if skipped < skip_bytes => {
// Skip the next page and consider it one character
skipped = skipped.saturating_add(1);
None
}
Op::CursorOffset(_) if skipped < skip_bytes => {
// Skip any offsets
None
}
op_to_pass_through => Some(op_to_pass_through.clone()),
}
};
if let Some(op) = real_op {
match op {
// Changing color
Op::Color(color) => {
self.style.text_color = color;
}
// Changing font
Op::Font(font) => {
self.style.text_font = font;
}
// Changing line/text alignment
Op::LineAlignment(line_alignment) => {
self.style.line_alignment = line_alignment;
}
// Moving the cursor
Op::CursorOffset(offset) => {
cursor.x += offset.x;
cursor.y += offset.y;
}
// Moving to the next page
Op::NextPage => {
// Pretending that nothing more fits on current page to force
// continuing on the next one
// Making that to account for one character for pagination purposes
total_processed_chars += 1;
return LayoutFit::OutOfBounds {
processed_chars: total_processed_chars,
height: self.layout_height(init_cursor, *cursor),
};
}
// Drawing text or icon
Op::Text(_) | Op::Icon(_) => {
// Text and Icon behave similarly - we try to fit them
// on the current page and if they do not fit,
// return the appropriate OutOfBounds message
let fit = if let Op::Text(to_display) = op {
let text = to_display.text.as_ref();
let text_len = to_display.length;
let start = text.len() - text_len;
let to_really_display = &text[start..];
// let to_really_display = text.text[text.start..text.end].to_string();
self.layout_text(to_really_display, cursor, sink)
} else if let Op::Icon(icon) = op {
self.layout_icon(icon, cursor, sink)
} else {
panic!("unexpected op type");
};
match fit {
LayoutFit::Fitting {
processed_chars, ..
} => {
total_processed_chars += processed_chars;
}
LayoutFit::OutOfBounds {
processed_chars, ..
} => {
total_processed_chars += processed_chars;
return LayoutFit::OutOfBounds {
processed_chars: total_processed_chars,
height: self.layout_height(init_cursor, *cursor),
};
}
}
}
}
}
}
LayoutFit::Fitting {
processed_chars: total_processed_chars,
height: self.layout_height(init_cursor, *cursor),
}
}
/// Loop through the `text` and try to fit it on the current screen,
/// reporting events to `sink`, which may do something with them (e.g. draw
/// on screen).
pub fn layout_text(
&self,
text: &str,
cursor: &mut Point,
sink: &mut dyn LayoutSink,
) -> LayoutFit {
let init_cursor = *cursor;
let mut remaining_text = text;
// Check if bounding box is high enough for at least one line.
if cursor.y > self.bottom_y() {
sink.out_of_bounds();
return LayoutFit::OutOfBounds {
processed_chars: 0,
height: 0,
};
}
while !remaining_text.is_empty() {
let span = Span::fit_horizontally(
remaining_text,
self.bounds.x1 - cursor.x,
self.style.text_font,
self.style.line_breaking,
);
// Report the span at the cursor position.
// Not doing it when the span length is 0, as that
// means we encountered a newline/line-break, which we do not draw.
// Line-breaks are reported later.
if span.length > 0 {
sink.text(*cursor, self, &remaining_text[..span.length]);
}
// Continue with the rest of the remaining_text.
remaining_text = &remaining_text[span.length + span.skip_next_chars..];
// Advance the cursor horizontally.
cursor.x += span.advance.x;
if span.advance.y > 0 {
// We're advancing to the next line.
// Check if we should be appending a hyphen at this point.
if span.insert_hyphen_before_line_break {
sink.hyphen(*cursor, self);
}
// Check the amount of vertical space we have left.
if cursor.y + span.advance.y > self.bottom_y() {
if !remaining_text.is_empty() {
// Append ellipsis to indicate more content is available, but only if we
// haven't already appended a hyphen.
let should_append_ellipsis =
matches!(self.style.page_breaking, PageBreaking::CutAndInsertEllipsis)
&& !span.insert_hyphen_before_line_break;
if should_append_ellipsis {
sink.ellipsis(*cursor, self);
}
// TODO: This does not work in case we are the last
// fitting text token on the line, with more text tokens
// following and `text.is_empty() == true`.
}
// Report we are out of bounds and quit.
sink.out_of_bounds();
return LayoutFit::OutOfBounds {
processed_chars: text.len() - remaining_text.len(),
height: self.layout_height(init_cursor, *cursor),
};
} else {
// Advance the cursor to the beginning of the next line.
cursor.x = self.bounds.x0;
cursor.y += span.advance.y;
// Report a line break. While rendering works using the cursor coordinates, we
// use explicit line-break reporting in the `Trace` impl.
sink.line_break(*cursor);
}
}
}
LayoutFit::Fitting {
processed_chars: text.len(),
height: self.layout_height(init_cursor, *cursor),
}
}
/// Try to fit the `icon` on the current screen
pub fn layout_icon(
&self,
icon: Icon,
cursor: &mut Point,
sink: &mut dyn LayoutSink,
) -> LayoutFit {
// Check if bounding box is high enough for at least one line.
if cursor.y > self.bottom_y() {
sink.out_of_bounds();
return LayoutFit::OutOfBounds {
processed_chars: 0,
height: 0,
};
}
// Icon is too wide to fit on current line.
// Trying to accommodate it on the next line, when it exists on this page.
if cursor.x + icon.width() > self.right_x() {
cursor.x = self.bounds.x0;
cursor.y += self.style.text_font.line_height();
if cursor.y > self.bottom_y() {
sink.out_of_bounds();
return LayoutFit::OutOfBounds {
processed_chars: 0,
height: self.style.text_font.line_height(),
};
}
}
sink.icon(*cursor, self, icon);
// TODO: currently we are using just small icons - that fit nicely to one line -
// but in case we would do bigger ones, we would need some anti-collision
// mechanism.
cursor.x += icon.width() as i16;
LayoutFit::Fitting {
// TODO: how to handle this? It could collide with "skip_first_n_bytes"
processed_chars: 1,
height: 0, // it should just draw on one line
}
}
/// Overall height of the content, including paddings.
fn layout_height(&self, init_cursor: Point, end_cursor: Point) -> i16 {
self.padding_top
+ self.style.text_font.text_height()
+ (end_cursor.y - init_cursor.y)
+ self.padding_bottom
}
}
/// Whether we can fit content on the current screen.
/// Knows how many characters got processed and how high the content is.
pub enum LayoutFit {
/// Entire content fits. Vertical size is returned in `height`.
Fitting { processed_chars: usize, height: i16 },
/// Content fits partially or not at all.
OutOfBounds { processed_chars: usize, height: i16 },
}
impl LayoutFit {
/// How high is the processed/fitted content.
pub fn height(&self) -> i16 {
match self {
LayoutFit::Fitting { height, .. } => *height,
LayoutFit::OutOfBounds { height, .. } => *height,
}
}
}
// TODO: LayoutSink could support even things like drawing icons
// or making custom x or y offsets from any position
/// Visitor for text segment operations.
/// Defines responses for certain kind of events encountered
/// when processing the content.
pub trait LayoutSink {
/// Text should be processed.
fn text(&mut self, _cursor: Point, _layout: &TextLayout, _text: &str) {}
/// Text should be processed.
fn icon(&mut self, _cursor: Point, _layout: &TextLayout, _icon: Icon) {}
/// Hyphen at the end of line.
fn hyphen(&mut self, _cursor: Point, _layout: &TextLayout) {}
/// Ellipsis at the end of the page.
fn ellipsis(&mut self, _cursor: Point, _layout: &TextLayout) {}
/// Line break - a newline.
fn line_break(&mut self, _cursor: Point) {}
/// Content cannot fit on the screen.
fn out_of_bounds(&mut self) {}
}
/// `LayoutSink` without any functionality.
/// Used to consume events when counting pages
/// or navigating to a certain page number.
pub struct TextNoOp;
impl LayoutSink for TextNoOp {}
/// `LayoutSink` for rendering the content.
pub struct TextRenderer;
impl LayoutSink for TextRenderer {
fn text(&mut self, cursor: Point, layout: &TextLayout, text: &str) {
// Accounting for the line-alignment - left, right or center.
// Assume the current line can be drawn on from the cursor
// to the right side of the screen.
match layout.style.line_alignment {
LineAlignment::Left => {
display::text(
cursor,
text,
layout.style.text_font,
layout.style.text_color,
layout.style.background_color,
);
}
LineAlignment::Center => {
let center = Point::new(cursor.x + (layout.bounds.x1 - cursor.x) / 2, cursor.y);
display::text_center(
center,
text,
layout.style.text_font,
layout.style.text_color,
layout.style.background_color,
);
}
LineAlignment::Right => {
let right = Point::new(layout.bounds.x1, cursor.y);
display::text_right(
right,
text,
layout.style.text_font,
layout.style.text_color,
layout.style.background_color,
);
}
}
}
fn hyphen(&mut self, cursor: Point, layout: &TextLayout) {
display::text(
cursor,
"-",
layout.style.text_font,
layout.style.hyphen_color,
layout.style.background_color,
);
}
fn ellipsis(&mut self, cursor: Point, layout: &TextLayout) {
display::text(
cursor,
"...",
layout.style.text_font,
layout.style.ellipsis_color,
layout.style.background_color,
);
}
fn icon(&mut self, cursor: Point, layout: &TextLayout, icon: Icon) {
icon.draw_bottom_left(
cursor,
layout.style.text_color,
layout.style.background_color,
);
}
}
/// `LayoutSink` for debugging purposes.
#[cfg(feature = "ui_debug")]
pub struct TraceSink<'a>(pub &'a mut dyn crate::trace::Tracer);
#[cfg(feature = "ui_debug")]
use crate::trace::Trace;
#[cfg(feature = "ui_debug")]
impl<'a> LayoutSink for TraceSink<'a> {
fn text(&mut self, _cursor: Point, _layout: &TextLayout, text: &str) {
self.0.string(text);
}
fn icon(&mut self, _cursor: Point, _layout: &TextLayout, icon: Icon) {
icon.trace(self.0);
}
fn hyphen(&mut self, _cursor: Point, _layout: &TextLayout) {
self.0.string("-");
}
fn ellipsis(&mut self, _cursor: Point, _layout: &TextLayout) {
self.0.string("...");
}
fn line_break(&mut self, _cursor: Point) {
self.0.string("\n");
}
}
pub trait GlyphMetrics {
fn char_width(&self, ch: char) -> i16;
fn line_height(&self) -> i16;
}
impl GlyphMetrics for Font {
fn char_width(&self, ch: char) -> i16 {
Font::char_width(*self, ch)
}
fn line_height(&self) -> i16 {
Font::line_height(*self)
}
}
/// Carries info about the content that was processed
/// on the current line.
#[derive(Debug, PartialEq, Eq)]
pub struct Span {
/// How many characters from the input text this span is laying out.
pub length: usize,
/// How many chars from the input text should we skip before fitting the
/// next span?
pub skip_next_chars: usize,
/// By how much to offset the cursor after this span. If the vertical offset
/// is bigger than zero, it means we are breaking the line.
pub advance: Offset,
/// If we are breaking the line, should we insert a hyphen right after this
/// span to indicate a word-break?
pub insert_hyphen_before_line_break: bool,
}
impl Span {
fn fit_horizontally(
text: &str,
max_width: i16,
text_font: impl GlyphMetrics,
breaking: LineBreaking,
) -> Self {
const ASCII_LF: char = '\n';
const ASCII_CR: char = '\r';
const ASCII_SPACE: char = ' ';
const ASCII_HYPHEN: char = '-';
fn is_whitespace(ch: char) -> bool {
ch == ASCII_SPACE || ch == ASCII_LF || ch == ASCII_CR
}
let hyphen_width = text_font.char_width(ASCII_HYPHEN);
// The span we return in case the line has to break. We mutate it in the
// possible break points, and its initial value is returned in case no text
// at all is fitting the constraints: zero length, zero width, full line
// break.
let mut line = Self {
length: 0,
advance: Offset::y(text_font.line_height()),
insert_hyphen_before_line_break: false,
skip_next_chars: 0,
};
let mut span_width = 0;
let mut found_any_whitespace = false;
let mut char_indices_iter = text.char_indices().peekable();
// Iterating manually because we need a reference to the iterator inside the
// loop.
while let Some((i, ch)) = char_indices_iter.next() {
let char_width = text_font.char_width(ch);
// Consider if we could be breaking the line at this position.
if is_whitespace(ch) {
// Break before the whitespace, without hyphen.
line.length = i;
line.advance.x = span_width;
line.insert_hyphen_before_line_break = false;
line.skip_next_chars = 1;
if ch == ASCII_CR {
// We'll be breaking the line, but advancing the cursor only by a half of the
// regular line height.
line.advance.y = text_font.line_height() / 2;
}
if ch == ASCII_LF || ch == ASCII_CR {
// End of line, break immediately.
return line;
}
found_any_whitespace = true;
} else if span_width + char_width > max_width {
// Cannot fit on this line. Return the last breakpoint.
return line;
} else {
let have_space_for_break = span_width + char_width + hyphen_width <= max_width;
let can_break_word = matches!(breaking, LineBreaking::BreakWordsAndInsertHyphen)
|| !found_any_whitespace;
if have_space_for_break && can_break_word {
// Break after this character, append hyphen.
line.length = match char_indices_iter.peek() {
Some((idx, _)) => *idx,
None => text.len(),
};
line.advance.x = span_width + char_width;
line.insert_hyphen_before_line_break = true;
line.skip_next_chars = 0;
}
}
span_width += char_width;
}
// The whole text is fitting on the current line.
Self {
length: text.len(),
advance: Offset::x(span_width),
insert_hyphen_before_line_break: false,
skip_next_chars: 0,
}
}
}

View File

@ -1,13 +1,15 @@
use super::theme;
use super::{common, theme};
use crate::ui::{
component::{Child, Component, Event, EventCtx},
display::{self, Font},
geometry::{Insets, Offset, Rect},
geometry::{Insets, Rect},
};
/// Component for holding another component and displaying
/// a title and optionally a subtitle describing that child component.
pub struct Frame<T, U> {
area: Rect,
title: U,
subtitle: Option<U>,
content: Child<T>,
}
@ -16,9 +18,10 @@ where
T: Component,
U: AsRef<str>,
{
pub fn new(title: U, content: T) -> Self {
pub fn new(title: U, subtitle: Option<U>, content: T) -> Self {
Self {
title,
subtitle,
area: Rect::zero(),
content: Child::new(content),
}
@ -37,10 +40,11 @@ where
type Msg = T::Msg;
fn place(&mut self, bounds: Rect) -> Rect {
const TITLE_SPACE: i16 = 4;
// Depending on whether there is subtitle or not
let title_space = if self.subtitle.is_some() { 12 } else { 4 };
let (title_area, content_area) = bounds.split_top(Font::BOLD.line_height());
let content_area = content_area.inset(Insets::top(TITLE_SPACE));
let (title_area, content_area) = bounds.split_top(theme::FONT_HEADER.line_height());
let content_area = content_area.inset(Insets::top(title_space));
self.area = title_area;
self.content.place(content_area);
@ -52,14 +56,7 @@ where
}
fn paint(&mut self) {
display::text(
self.area.bottom_left() - Offset::y(2),
self.title.as_ref(),
Font::BOLD,
theme::FG,
theme::BG,
);
display::dotted_line(self.area.bottom_left(), self.area.width(), theme::FG);
common::paint_header(self.area.top_left(), &self.title, self.subtitle.as_ref());
self.content.paint();
}
}
@ -72,7 +69,10 @@ where
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("Frame");
t.field("title", &self.title);
t.title(self.title.as_ref());
if let Some(ref subtitle) = self.subtitle {
t.title(subtitle.as_ref());
}
t.field("content", &self.content);
t.close();
}

View File

@ -3,8 +3,9 @@ use crate::{
ui::{
animation::Animation,
component::{Component, Event, EventCtx},
display::{self, Color, Font},
display::{self, Color, Font, Icon},
geometry::{Offset, Rect},
model_tr::theme,
},
};
@ -20,31 +21,82 @@ enum State {
Grown,
}
pub struct Loader {
pub struct Loader<T> {
area: Rect,
state: State,
growing_duration: Duration,
shrinking_duration: Duration,
text: display::TextOverlay<'static>,
text_overlay: Option<display::TextOverlay<T>>,
styles: LoaderStyleSheet,
}
impl Loader {
impl<T: AsRef<str>> Loader<T> {
pub const SIZE: Offset = Offset::new(120, 120);
pub fn new(text: &'static str, styles: LoaderStyleSheet) -> Self {
let overlay = display::TextOverlay::new(text, styles.normal.font);
pub fn new(text_overlay: Option<display::TextOverlay<T>>, styles: LoaderStyleSheet) -> Self {
Self {
area: Rect::zero(),
state: State::Initial,
growing_duration: Duration::from_millis(1000),
shrinking_duration: Duration::from_millis(500),
text: overlay,
text_overlay,
styles,
}
}
pub fn text(text: T, styles: LoaderStyleSheet) -> Self {
let text_overlay = display::TextOverlay::new(text, styles.normal.font);
Self::new(Some(text_overlay), styles)
}
// TODO: support the icon drawing
pub fn icon(_icon: Icon, styles: LoaderStyleSheet) -> Self {
Self::new(None, styles)
}
pub fn with_growing_duration(mut self, growing_duration: Duration) -> Self {
self.growing_duration = growing_duration;
self
}
/// Change the duration of the loader.
pub fn set_duration(&mut self, growing_duration: Duration) {
self.growing_duration = growing_duration;
}
pub fn get_duration(&self) -> Duration {
self.growing_duration
}
pub fn get_text(&self) -> &T {
self.text_overlay
.as_ref()
.expect("Loader does not have text")
.get_text()
}
/// Change the text of the loader.
/// When the text_overlay does not exist (as it was created by icon),
/// create it and place it
pub fn set_text(&mut self, text: T) {
if let Some(text_overlay) = &mut self.text_overlay {
text_overlay.set_text(text);
} else {
let text = display::TextOverlay::new(text, self.styles.normal.font);
self.text_overlay = Some(text);
if let Some(text_overlay) = &mut self.text_overlay {
let baseline = self.area.bottom_center() + Offset::new(1, -1);
text_overlay.place(baseline);
}
}
}
/// Return width of given text according to current style.
pub fn get_text_width(&self, text: &T) -> i16 {
self.styles.normal.font.text_width(text.as_ref())
}
pub fn start_growing(&mut self, ctx: &mut EventCtx, now: Instant) {
let mut anim = Animation::new(
display::LOADER_MIN,
@ -111,27 +163,35 @@ impl Loader {
matches!(self.progress(now), Some(display::LOADER_MIN))
}
pub fn paint_loader(&mut self, style: &LoaderStyle, done: i16) {
let invert_from = ((self.area.width() + 1) * done) / (display::LOADER_MAX as i16);
pub fn paint_loader(&mut self, style: &LoaderStyle, done: i32) {
// TODO: support painting icons
if let Some(text_overlay) = &mut self.text_overlay {
// NOTE: need to calculate this in `i32`, it would overflow using `i16`
let invert_from = ((self.area.width() as i32 + 1) * done) / (display::LOADER_MAX as i32);
display::bar_with_text_and_fill(
self.area,
Some(self.text),
style.fg_color,
style.bg_color,
-1,
invert_from,
);
// TODO: the text should be moved one pixel to the top so it is centered in the
// loader
display::bar_with_text_and_fill(
self.area,
Some(text_overlay),
style.fg_color,
style.bg_color,
-1,
invert_from as i16,
);
}
}
}
impl Component for Loader {
impl<T: AsRef<str>> Component for Loader<T> {
type Msg = LoaderMsg;
fn place(&mut self, bounds: Rect) -> Rect {
self.area = bounds;
let baseline = bounds.bottom_center() + Offset::new(1, -1);
self.text.place(baseline);
if let Some(text_overlay) = &mut self.text_overlay {
let baseline = bounds.bottom_center() + Offset::new(1, -1);
text_overlay.place(baseline);
}
self.area
}
@ -140,18 +200,19 @@ impl Component for Loader {
if let Event::Timer(EventCtx::ANIM_FRAME_TIMER) = event {
if self.is_animating() {
// We have something to paint, so request to be painted in the next pass.
ctx.request_paint();
if self.is_completely_grown(now) {
self.state = State::Grown;
ctx.request_paint();
return Some(LoaderMsg::GrownCompletely);
} else if self.is_completely_shrunk(now) {
self.state = State::Initial;
ctx.request_paint();
return Some(LoaderMsg::ShrunkCompletely);
} else {
// There is further progress in the animation, request an animation frame event.
ctx.request_anim_frame();
// We have something to paint, so request to be painted in the next pass.
ctx.request_paint();
}
}
}
@ -169,11 +230,11 @@ impl Component for Loader {
if let State::Initial = self.state {
self.paint_loader(self.styles.normal, 0);
} else if let State::Grown = self.state {
self.paint_loader(self.styles.normal, display::LOADER_MAX as i16);
self.paint_loader(self.styles.normal, display::LOADER_MAX as i32);
} else {
let progress = self.progress(now);
if let Some(done) = progress {
self.paint_loader(self.styles.normal, done as i16);
self.paint_loader(self.styles.normal, done as i32);
} else {
self.paint_loader(self.styles.normal, 0);
}
@ -191,8 +252,20 @@ pub struct LoaderStyle {
pub bg_color: Color,
}
impl LoaderStyleSheet {
pub fn default() -> Self {
Self {
normal: &LoaderStyle {
font: theme::FONT_BUTTON,
fg_color: theme::FG,
bg_color: theme::BG,
},
}
}
}
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for Loader {
impl<T: AsRef<str>> crate::trace::Trace for Loader<T> {
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
d.open("Loader");
d.close();

View File

@ -1,19 +1,47 @@
mod bip39;
mod button;
mod button_controller;
mod changing_text;
mod choice;
mod choice_item;
mod common;
mod confirm;
mod dialog;
mod flow;
mod flow_pages;
mod flow_pages_poc_helpers;
mod frame;
mod loader;
mod page;
mod passphrase;
mod pin;
mod result_anim;
mod result_popup;
mod scrollbar;
mod simple_choice;
use super::theme;
pub use button::{Button, ButtonContent, ButtonMsg, ButtonPos, ButtonStyle, ButtonStyleSheet};
pub use bip39::{Bip39Entry, Bip39EntryMsg};
pub use button::{
Button, ButtonAction, ButtonActions, ButtonContent, ButtonDetails, ButtonLayout, ButtonMsg,
ButtonPos, ButtonStyle, ButtonStyleSheet,
};
pub use confirm::{HoldToConfirm, HoldToConfirmMsg};
pub use button_controller::{ButtonController, ButtonControllerMsg};
pub use changing_text::ChangingTextLine;
pub use choice::{ChoiceFactory, ChoicePage, ChoicePageMsg};
pub use choice_item::{ChoiceItem, ChoiceItemAPI, MultilineTextChoiceItem, TextChoiceItem};
pub use dialog::{Dialog, DialogMsg};
pub use flow::{Flow, FlowMsg};
pub use flow_pages::{FlowPages, Page};
pub use frame::Frame;
pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet};
pub use page::ButtonPage;
pub use passphrase::{PassphraseEntry, PassphraseEntryMsg};
pub use pin::{PinEntry, PinEntryMsg};
pub use result_anim::{ResultAnim, ResultAnimMsg};
pub use result_popup::{ResultPopup, ResultPopupMsg};
pub use scrollbar::ScrollBar;
pub use simple_choice::{SimpleChoice, SimpleChoiceMsg};

View File

@ -1,93 +1,209 @@
use crate::ui::{
component::{Component, ComponentExt, Event, EventCtx, Never, Pad, PageMsg, Paginate},
display::{self, Color, Font},
geometry::{Insets, Offset, Point, Rect},
use crate::{
micropython::buffer::StrBuffer,
ui::{
component::{Child, Component, ComponentExt, Event, EventCtx, Pad, PageMsg, Paginate},
display::Color,
geometry::{Insets, Rect},
},
};
use super::{theme, Button, ButtonMsg, ButtonPos};
use super::{
theme, ButtonController, ButtonControllerMsg, ButtonDetails, ButtonLayout, ButtonPos, ScrollBar,
};
pub struct ButtonPage<T> {
content: T,
scrollbar: ScrollBar,
pub struct ButtonPage<S, T> {
content: Child<T>,
scrollbar: Child<ScrollBar>,
pad: Pad,
prev: Button<&'static str>,
next: Button<&'static str>,
cancel: Button<&'static str>,
confirm: Button<&'static str>,
cancel_btn_details: Option<ButtonDetails<S>>,
confirm_btn_details: Option<ButtonDetails<S>>,
back_btn_details: Option<ButtonDetails<S>>,
next_btn_details: Option<ButtonDetails<S>>,
buttons: Child<ButtonController<S>>,
}
impl<T> ButtonPage<T>
impl<T> ButtonPage<&'static str, T>
where
T: Paginate,
T: Component,
{
pub fn new(content: T, background: Color) -> Self {
/// Constructor for `&'static str` button-text type.
pub fn new_str(content: T, background: Color) -> Self {
Self {
content,
scrollbar: ScrollBar::vertical(),
content: Child::new(content),
scrollbar: Child::new(ScrollBar::vertical_to_be_filled_later()),
pad: Pad::with_background(background),
prev: Button::with_text(ButtonPos::Left, "BACK", theme::button_cancel()),
next: Button::with_text(ButtonPos::Right, "NEXT", theme::button_default()),
cancel: Button::with_text(ButtonPos::Left, "CANCEL", theme::button_cancel()),
confirm: Button::with_text(ButtonPos::Right, "CONFIRM", theme::button_default()),
cancel_btn_details: Some(ButtonDetails::cancel_icon()),
confirm_btn_details: Some(ButtonDetails::text("CONFIRM")),
back_btn_details: Some(ButtonDetails::up_arrow_icon_wide()),
next_btn_details: Some(ButtonDetails::down_arrow_icon_wide()),
// Setting empty layout for now, we do not yet know the page count.
// Initial button layout will be set in `place()` after we can call
// `content.page_count()`.
buttons: Child::new(ButtonController::new(ButtonLayout::empty())),
}
}
fn change_page(&mut self, ctx: &mut EventCtx, page: usize) {
// Change the page in the content, clear the background under it and make sure
// it gets completely repainted.
self.content.change_page(page);
self.content.request_complete_repaint(ctx);
self.pad.clear();
}
}
impl<T> Component for ButtonPage<T>
impl<T> ButtonPage<StrBuffer, T>
where
T: Paginate,
T: Component,
{
/// Constructor for `StrBuffer` button-text type.
pub fn new_str_buf(content: T, background: Color) -> Self {
Self {
content: Child::new(content),
scrollbar: Child::new(ScrollBar::vertical_to_be_filled_later()),
pad: Pad::with_background(background),
cancel_btn_details: Some(ButtonDetails::cancel_icon()),
confirm_btn_details: Some(ButtonDetails::text("CONFIRM".into())),
back_btn_details: Some(ButtonDetails::up_arrow_icon_wide()),
next_btn_details: Some(ButtonDetails::down_arrow_icon_wide()),
// Setting empty layout for now, we do not yet know the page count.
// Initial button layout will be set in `place()` after we can call
// `content.page_count()`.
buttons: Child::new(ButtonController::new(ButtonLayout::empty())),
}
}
}
impl<S, T> ButtonPage<S, T>
where
T: Paginate,
T: Component,
S: AsRef<str>,
S: Clone,
{
pub fn with_cancel_btn(mut self, btn_details: Option<ButtonDetails<S>>) -> Self {
self.cancel_btn_details = btn_details;
self
}
pub fn with_confirm_btn(mut self, btn_details: Option<ButtonDetails<S>>) -> Self {
self.confirm_btn_details = btn_details;
self
}
pub fn with_back_btn(mut self, btn_details: Option<ButtonDetails<S>>) -> Self {
self.back_btn_details = btn_details;
self
}
pub fn with_next_btn(mut self, btn_details: Option<ButtonDetails<S>>) -> Self {
self.next_btn_details = btn_details;
self
}
/// Basically just determining whether the right button for
/// initial page should be "NEXT" or "CONFIRM".
/// Can only be called when we know the final page_count.
fn set_buttons_for_initial_page(&mut self, page_count: usize) {
let btn_layout = self.get_button_layout(false, page_count > 1);
self.buttons = Child::new(ButtonController::new(btn_layout));
}
/// Called when user pressed "BACK" or "NEXT".
/// Change the page in the content, clear the background under it and make
/// sure it gets completely repainted. Also updating the buttons.
fn change_page(&mut self, ctx: &mut EventCtx) {
let active_page = self.scrollbar.inner().active_page;
self.content.inner_mut().change_page(active_page);
self.content.request_complete_repaint(ctx);
self.scrollbar.request_complete_repaint(ctx);
self.update_buttons(ctx);
self.pad.clear();
}
/// Reflecting the current page in the buttons.
fn update_buttons(&mut self, ctx: &mut EventCtx) {
let btn_layout = self.get_button_layout(
self.scrollbar.inner().has_previous_page(),
self.scrollbar.inner().has_next_page(),
);
self.buttons.mutate(ctx, |_ctx, buttons| {
buttons.set(btn_layout);
});
}
fn get_button_layout(&self, has_prev: bool, has_next: bool) -> ButtonLayout<S> {
let btn_left = self.get_left_button_details(has_prev);
let btn_right = self.get_right_button_details(has_next);
ButtonLayout::new(btn_left, None, btn_right)
}
fn get_left_button_details(&self, has_prev_page: bool) -> Option<ButtonDetails<S>> {
if has_prev_page {
self.back_btn_details.clone()
} else {
self.cancel_btn_details.clone()
}
}
fn get_right_button_details(&self, has_next_page: bool) -> Option<ButtonDetails<S>> {
if has_next_page {
self.next_btn_details.clone()
} else {
self.confirm_btn_details.clone()
}
}
}
impl<S, T> Component for ButtonPage<S, T>
where
S: Clone,
S: AsRef<str>,
T: Component,
T: Paginate,
{
type Msg = PageMsg<T::Msg, bool>;
fn place(&mut self, bounds: Rect) -> Rect {
let button_height = Font::BOLD.line_height() + 2;
let (content_area, button_area) = bounds.split_bottom(button_height);
let (content_area, scrollbar_area) = content_area.split_right(ScrollBar::WIDTH);
let (content_and_scrollbar_area, button_area) = bounds.split_bottom(theme::BUTTON_HEIGHT);
let (content_area, scrollbar_area) =
content_and_scrollbar_area.split_right(ScrollBar::WIDTH);
let content_area = content_area.inset(Insets::top(1));
self.pad.place(bounds);
// Do not pad the button area nor the scrollbar, leave it to them
self.pad.place(content_area);
self.content.place(content_area);
let page_count = self.content.page_count();
self.scrollbar.set_count_and_active_page(page_count, 0);
// Need to be called here, only after content is placed
// and we can calculate the page count
let page_count = self.content.inner_mut().page_count();
self.scrollbar.inner_mut().set_page_count(page_count);
self.scrollbar.place(scrollbar_area);
self.prev.place(button_area);
self.next.place(button_area);
self.cancel.place(button_area);
self.confirm.place(button_area);
self.set_buttons_for_initial_page(page_count);
self.buttons.place(button_area);
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
ctx.set_page_count(self.scrollbar.page_count);
if self.scrollbar.has_previous_page() {
if let Some(ButtonMsg::Clicked) = self.prev.event(ctx, event) {
// Scroll up.
self.scrollbar.go_to_previous_page();
self.change_page(ctx, self.scrollbar.active_page);
return None;
}
} else if let Some(ButtonMsg::Clicked) = self.cancel.event(ctx, event) {
return Some(PageMsg::Controls(false));
}
ctx.set_page_count(self.scrollbar.inner().page_count);
let button_event = self.buttons.event(ctx, event);
if self.scrollbar.has_next_page() {
if let Some(ButtonMsg::Clicked) = self.next.event(ctx, event) {
// Scroll down.
self.scrollbar.go_to_next_page();
self.change_page(ctx, self.scrollbar.active_page);
return None;
if let Some(ButtonControllerMsg::Triggered(pos)) = button_event {
match pos {
ButtonPos::Left => {
if self.scrollbar.inner().has_previous_page() {
// Clicked BACK. Scroll up.
self.scrollbar.inner_mut().go_to_previous_page();
self.change_page(ctx);
} else {
// Clicked CANCEL. Send result.
return Some(PageMsg::Controls(false));
}
}
ButtonPos::Right => {
if self.scrollbar.inner().has_next_page() {
// Clicked NEXT. Scroll down.
self.scrollbar.inner_mut().go_to_next_page();
self.change_page(ctx);
} else {
// Clicked CONFIRM. Send result.
return Some(PageMsg::Controls(true));
}
}
_ => {}
}
} else if let Some(ButtonMsg::Clicked) = self.confirm.event(ctx, event) {
return Some(PageMsg::Controls(true));
}
if let Some(msg) = self.content.event(ctx, event) {
@ -100,128 +216,60 @@ where
self.pad.paint();
self.content.paint();
self.scrollbar.paint();
if self.scrollbar.has_previous_page() {
self.prev.paint();
} else {
self.cancel.paint();
}
if self.scrollbar.has_next_page() {
self.next.paint();
} else {
self.confirm.paint();
}
self.buttons.paint();
}
}
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for ButtonPage<T>
use super::ButtonAction;
#[cfg(feature = "ui_debug")]
use heapless::String;
#[cfg(feature = "ui_debug")]
impl<S, T> crate::trace::Trace for ButtonPage<S, T>
where
T: crate::trace::Trace,
S: AsRef<str>,
{
fn get_btn_action(&self, pos: ButtonPos) -> String<25> {
match pos {
ButtonPos::Left => {
if self.scrollbar.inner().has_previous_page() {
ButtonAction::PrevPage.string()
} else if self.cancel_btn_details.is_some() {
ButtonAction::Cancel.string()
} else {
ButtonAction::empty()
}
}
ButtonPos::Right => {
if self.scrollbar.inner().has_next_page() {
ButtonAction::NextPage.string()
} else if self.confirm_btn_details.is_some() {
ButtonAction::Confirm.string()
} else {
ButtonAction::empty()
}
}
ButtonPos::Middle => ButtonAction::empty(),
}
}
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("ButtonPage");
t.field("active_page", &self.scrollbar.active_page);
t.field("page_count", &self.scrollbar.page_count);
t.kw_pair(
"active_page",
inttostr!(self.scrollbar.inner().active_page as u8),
);
t.kw_pair(
"page_count",
inttostr!(self.scrollbar.inner().page_count as u8),
);
self.report_btn_actions(t);
// TODO: it seems the button text is not updated when paginating (but actions
// above are)
t.field("buttons", &self.buttons);
t.field("content", &self.content);
t.close();
}
}
pub struct ScrollBar {
area: Rect,
page_count: usize,
active_page: usize,
}
impl ScrollBar {
pub const WIDTH: i16 = 8;
pub const DOT_SIZE: Offset = Offset::new(4, 4);
pub const DOT_INTERVAL: i16 = 6;
pub fn vertical() -> Self {
Self {
area: Rect::zero(),
page_count: 0,
active_page: 0,
}
}
pub fn set_count_and_active_page(&mut self, page_count: usize, active_page: usize) {
self.page_count = page_count;
self.active_page = active_page;
}
pub fn has_next_page(&self) -> bool {
self.active_page < self.page_count - 1
}
pub fn has_previous_page(&self) -> bool {
self.active_page > 0
}
pub fn go_to_next_page(&mut self) {
self.active_page = self.active_page.saturating_add(1).min(self.page_count - 1);
}
pub fn go_to_previous_page(&mut self) {
self.active_page = self.active_page.saturating_sub(1);
}
fn paint_dot(&self, active: bool, top_left: Point) {
let sides = [
Rect::from_top_left_and_size(top_left + Offset::x(1), Offset::new(2, 1)),
Rect::from_top_left_and_size(top_left + Offset::y(1), Offset::new(1, 2)),
Rect::from_top_left_and_size(
top_left + Offset::new(1, Self::DOT_SIZE.y - 1),
Offset::new(2, 1),
),
Rect::from_top_left_and_size(
top_left + Offset::new(Self::DOT_SIZE.x - 1, 1),
Offset::new(1, 2),
),
];
for side in sides {
display::rect_fill(side, theme::FG)
}
if active {
display::rect_fill(
Rect::from_top_left_and_size(top_left, Self::DOT_SIZE).inset(Insets::uniform(1)),
theme::FG,
)
}
}
}
impl Component for ScrollBar {
type Msg = Never;
fn place(&mut self, bounds: Rect) -> Rect {
self.area = bounds;
self.area
}
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
None
}
fn paint(&mut self) {
let count = self.page_count as i16;
let interval = {
let available_height = self.area.height();
let naive_height = count * Self::DOT_INTERVAL;
if naive_height > available_height {
available_height / count
} else {
Self::DOT_INTERVAL
}
};
let mut dot = Point::new(
self.area.center().x - Self::DOT_SIZE.x / 2,
self.area.center().y - (count / 2) * interval,
);
for i in 0..self.page_count {
self.paint_dot(i == self.active_page, dot);
dot.y += interval
}
}
}

View File

@ -0,0 +1,365 @@
use crate::{
time::Duration,
ui::{
component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx},
geometry::Rect,
},
};
use super::{
choice_item::BigCharacterChoiceItem, ButtonDetails, ButtonLayout, ChangingTextLine,
ChoiceFactory, ChoiceItem, ChoicePage, ChoicePageMsg, MultilineTextChoiceItem, TextChoiceItem,
};
use heapless::String;
pub enum PassphraseEntryMsg {
Confirmed,
Cancelled,
}
/// Defines the choices currently available on the screen
#[derive(PartialEq, Clone)]
enum ChoiceCategory {
Menu,
LowercaseLetter,
UppercaseLetter,
Digit,
SpecialSymbol,
}
const MAX_PASSPHRASE_LENGTH: usize = 50;
const HOLD_DURATION: Duration = Duration::from_secs(1);
const DIGITS: [char; 10] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
const LOWERCASE_LETTERS: [char; 26] = [
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's',
't', 'u', 'v', 'w', 'x', 'y', 'z',
];
const UPPERCASE_LETTERS: [char; 26] = [
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
];
const SPECIAL_SYMBOLS: [char; 30] = [
'_', '<', '>', '.', ':', '@', '/', '|', '\\', '!', '(', ')', '+', '%', '&', '-', '[', ']', '?',
'{', '}', ',', '\'', '`', ';', '"', '~', '$', '^', '=',
];
const MENU_LENGTH: usize = 6;
const DEL_INDEX: usize = MENU_LENGTH - 1;
const SHOW_INDEX: usize = MENU_LENGTH - 2;
const MENU: [&str; MENU_LENGTH] = ["abc", "ABC", "123", "*#_", "SHOW PASS", "DEL LAST CHAR"];
/// Get a character at a specified index for a specified category.
fn get_char(current_category: &ChoiceCategory, index: u8) -> char {
let index = index as usize;
match current_category {
ChoiceCategory::LowercaseLetter => LOWERCASE_LETTERS[index],
ChoiceCategory::UppercaseLetter => UPPERCASE_LETTERS[index],
ChoiceCategory::Digit => DIGITS[index],
ChoiceCategory::SpecialSymbol => SPECIAL_SYMBOLS[index],
ChoiceCategory::Menu => unreachable!(),
}
}
/// Return category from menu based on page index.
fn get_category_from_menu(page_index: u8) -> ChoiceCategory {
match page_index {
0 => ChoiceCategory::LowercaseLetter,
1 => ChoiceCategory::UppercaseLetter,
2 => ChoiceCategory::Digit,
3 => ChoiceCategory::SpecialSymbol,
_ => unreachable!(),
}
}
/// How many choices are available for a specified category.
/// (does not count the extra MENU choice for characters)
fn get_category_length(current_category: &ChoiceCategory) -> u8 {
match current_category {
ChoiceCategory::LowercaseLetter => LOWERCASE_LETTERS.len() as u8,
ChoiceCategory::UppercaseLetter => UPPERCASE_LETTERS.len() as u8,
ChoiceCategory::Digit => DIGITS.len() as u8,
ChoiceCategory::SpecialSymbol => SPECIAL_SYMBOLS.len() as u8,
ChoiceCategory::Menu => MENU.len() as u8,
}
}
/// Whether this index is the MENU index - the last one in the list.
fn is_menu_choice(current_category: &ChoiceCategory, page_index: u8) -> bool {
if let ChoiceCategory::Menu = current_category {
unreachable!()
}
let category_length = get_category_length(current_category);
page_index == category_length
}
struct ChoiceFactoryPassphrase {
current_category: ChoiceCategory,
}
impl ChoiceFactoryPassphrase {
fn new(current_category: ChoiceCategory) -> Self {
Self { current_category }
}
/// MENU choices with accept and cancel hold-to-confirm side buttons.
fn get_menu_item(&self, choice_index: u8) -> ChoiceItem {
let choice = MENU[choice_index as usize];
let item =
MultilineTextChoiceItem::new(String::from(choice), ButtonLayout::default_three_icons())
.use_delimiter(' ');
let mut menu_item = ChoiceItem::MultilineText(item);
// Including accept button on the left and cancel on the very right
// TODO: could have some icons instead of the shortcut text
if choice_index == 0 {
menu_item.set_left_btn(Some(
ButtonDetails::text("ACC").with_duration(HOLD_DURATION),
));
} else if choice_index == MENU.len() as u8 - 1 {
menu_item.set_right_btn(Some(
ButtonDetails::text("CAN").with_duration(HOLD_DURATION),
));
}
menu_item
}
/// Character choices with a MENU choice at the end (visible from start) to
/// return back
fn get_character_item(&self, choice_index: u8) -> ChoiceItem {
if is_menu_choice(&self.current_category, choice_index) {
let menu_choice =
TextChoiceItem::new("MENU", ButtonLayout::three_icons_middle_text("RETURN"));
ChoiceItem::Text(menu_choice)
} else {
let ch = get_char(&self.current_category, choice_index);
let char_choice = BigCharacterChoiceItem::new(ch, ButtonLayout::default_three_icons());
ChoiceItem::BigCharacter(char_choice)
}
}
}
impl ChoiceFactory for ChoiceFactoryPassphrase {
fn get(&self, choice_index: u8) -> ChoiceItem {
match self.current_category {
ChoiceCategory::Menu => self.get_menu_item(choice_index),
_ => self.get_character_item(choice_index),
}
}
fn count(&self) -> u8 {
let length = get_category_length(&self.current_category);
// All non-MENU categories have an extra item for returning back to MENU
match self.current_category {
ChoiceCategory::Menu => length,
_ => length + 1,
}
}
}
/// Component for entering a passphrase.
pub struct PassphraseEntry {
choice_page: ChoicePage<ChoiceFactoryPassphrase>,
passphrase_dots: Child<ChangingTextLine<String<MAX_PASSPHRASE_LENGTH>>>,
show_plain_passphrase: bool,
textbox: TextBox<MAX_PASSPHRASE_LENGTH>,
current_category: ChoiceCategory,
menu_position: u8, // position in the menu so we can return back
}
impl PassphraseEntry {
pub fn new() -> Self {
let menu_choices = ChoiceFactoryPassphrase::new(ChoiceCategory::Menu);
Self {
choice_page: ChoicePage::new(menu_choices),
passphrase_dots: Child::new(ChangingTextLine::center_mono(String::new())),
show_plain_passphrase: false,
textbox: TextBox::empty(),
current_category: ChoiceCategory::Menu,
menu_position: 0,
}
}
fn update_passphrase_dots(&mut self, ctx: &mut EventCtx) {
// TODO: when the passphrase is longer than fits the screen, we might show
// ellipsis
if self.show_plain_passphrase {
let passphrase = String::from(self.passphrase());
self.passphrase_dots.inner_mut().update_text(passphrase);
} else {
let mut dots: String<MAX_PASSPHRASE_LENGTH> = String::new();
for _ in 0..self.textbox.len() {
unwrap!(dots.push_str("*"));
}
self.passphrase_dots.inner_mut().update_text(dots);
}
self.passphrase_dots.request_complete_repaint(ctx);
}
fn append_char(&mut self, ctx: &mut EventCtx, ch: char) {
self.textbox.append(ctx, ch);
}
fn delete_last_digit(&mut self, ctx: &mut EventCtx) {
self.textbox.delete_last(ctx);
}
/// Displaying the MENU
fn show_menu_page(&mut self, ctx: &mut EventCtx) {
let menu_choices = ChoiceFactoryPassphrase::new(ChoiceCategory::Menu);
self.choice_page.reset(ctx, menu_choices, true, false);
// Going back to the last MENU position before showing the MENU
self.choice_page.set_page_counter(ctx, self.menu_position);
}
/// Displaying the character category
fn show_category_page(&mut self, ctx: &mut EventCtx) {
let category_choices = ChoiceFactoryPassphrase::new(self.current_category.clone());
self.choice_page.reset(ctx, category_choices, true, true);
}
pub fn passphrase(&self) -> &str {
self.textbox.content()
}
fn is_full(&self) -> bool {
self.textbox.is_full()
}
}
impl Component for PassphraseEntry {
type Msg = PassphraseEntryMsg;
fn place(&mut self, bounds: Rect) -> Rect {
let passphrase_area_height = self.passphrase_dots.inner().needed_height();
let (passphrase_area, choice_area) = bounds.split_top(passphrase_area_height);
self.passphrase_dots.place(passphrase_area);
self.choice_page.place(choice_area);
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
// Any event when showing real passphrase should hide it
if self.show_plain_passphrase {
self.show_plain_passphrase = false;
self.update_passphrase_dots(ctx);
}
let msg = self.choice_page.event(ctx, event);
if self.current_category == ChoiceCategory::Menu {
match msg {
// Going to new category, applying some action or returning the result
Some(ChoicePageMsg::Choice(page_counter)) => match page_counter as usize {
DEL_INDEX => {
self.delete_last_digit(ctx);
self.update_passphrase_dots(ctx);
ctx.request_paint();
}
SHOW_INDEX => {
self.show_plain_passphrase = true;
self.update_passphrase_dots(ctx);
ctx.request_paint();
}
_ => {
self.menu_position = page_counter;
self.current_category = get_category_from_menu(page_counter);
self.show_category_page(ctx);
ctx.request_paint();
}
},
Some(ChoicePageMsg::LeftMost) => return Some(PassphraseEntryMsg::Confirmed),
Some(ChoicePageMsg::RightMost) => return Some(PassphraseEntryMsg::Cancelled),
_ => {}
}
} else {
// Coming back to MENU or adding new character
if let Some(ChoicePageMsg::Choice(page_counter)) = msg {
if is_menu_choice(&self.current_category, page_counter) {
self.current_category = ChoiceCategory::Menu;
self.show_menu_page(ctx);
ctx.request_paint();
} else if !self.is_full() {
let new_char = get_char(&self.current_category, page_counter);
self.append_char(ctx, new_char);
self.update_passphrase_dots(ctx);
ctx.request_paint();
}
}
}
None
}
fn paint(&mut self) {
self.passphrase_dots.paint();
self.choice_page.paint();
}
}
#[cfg(feature = "ui_debug")]
use super::{ButtonAction, ButtonPos};
#[cfg(feature = "ui_debug")]
use crate::ui::util;
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for PassphraseEntry {
fn get_btn_action(&self, pos: ButtonPos) -> String<25> {
match pos {
ButtonPos::Left => match self.current_category {
ChoiceCategory::Menu => match self.choice_page.has_previous_choice() {
true => ButtonAction::PrevPage.string(),
false => ButtonAction::Confirm.string(),
},
_ => ButtonAction::PrevPage.string(),
},
ButtonPos::Right => match self.current_category {
ChoiceCategory::Menu => match self.choice_page.has_next_choice() {
true => ButtonAction::NextPage.string(),
false => ButtonAction::Cancel.string(),
},
_ => ButtonAction::NextPage.string(),
},
ButtonPos::Middle => {
let current_index = self.choice_page.page_index() as usize;
match &self.current_category {
ChoiceCategory::Menu => match current_index {
DEL_INDEX => ButtonAction::Action("Del last char").string(),
SHOW_INDEX => ButtonAction::Action("Show pass").string(),
_ => ButtonAction::select_item(MENU[current_index]),
},
_ => {
// There is "MENU" option at the end
match self.choice_page.has_next_choice() {
false => ButtonAction::Action("Back to MENU").string(),
true => {
let ch = match &self.current_category {
ChoiceCategory::LowercaseLetter => {
LOWERCASE_LETTERS[current_index]
}
ChoiceCategory::UppercaseLetter => {
UPPERCASE_LETTERS[current_index]
}
ChoiceCategory::Digit => DIGITS[current_index],
ChoiceCategory::SpecialSymbol => SPECIAL_SYMBOLS[current_index],
ChoiceCategory::Menu => unreachable!(),
};
ButtonAction::select_item(util::char_to_string::<1>(ch))
}
}
}
}
}
}
}
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("PassphraseEntry");
// NOTE: `show_plain_passphrase` was not able to be transferred,
// as it is true only for a very small amount of time
t.kw_pair("textbox", self.textbox.content());
self.report_btn_actions(t);
t.field("choice_page", &self.choice_page);
t.close();
}
}

View File

@ -0,0 +1,246 @@
use crate::{
micropython::buffer::StrBuffer,
trezorhal::random,
ui::{
component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx},
geometry::Rect,
},
};
use super::{
choice_item::BigCharacterChoiceItem, ButtonDetails, ButtonLayout, ChangingTextLine,
ChoiceFactory, ChoiceItem, ChoicePage, ChoicePageMsg, MultilineTextChoiceItem,
};
use heapless::String;
pub enum PinEntryMsg {
Confirmed,
Cancelled,
}
const MAX_PIN_LENGTH: usize = 50;
const MAX_VISIBLE_DOTS: usize = 18;
const MAX_VISIBLE_DIGITS: usize = 18;
const CHOICE_LENGTH: usize = 14;
const EXIT_INDEX: usize = 0;
const DELETE_INDEX: usize = 1;
const SHOW_INDEX: usize = 2;
const PROMPT_INDEX: usize = 3;
const CHOICES: [&str; CHOICE_LENGTH] = [
"EXIT",
"DELETE",
"SHOW PIN",
"PLACEHOLDER FOR THE PROMPT",
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
];
struct ChoiceFactoryPIN {
prompt: StrBuffer,
}
impl ChoiceFactoryPIN {
fn new(prompt: StrBuffer) -> Self {
Self { prompt }
}
}
impl ChoiceFactory for ChoiceFactoryPIN {
fn get(&self, choice_index: u8) -> ChoiceItem {
let choice = CHOICES[choice_index as usize];
// Depending on whether it is a digit (one character) or a text.
// Digits are BIG, the rest is multiline.
let mut choice_item = if choice.len() == 1 {
let item =
BigCharacterChoiceItem::from_str(choice, ButtonLayout::default_three_icons());
ChoiceItem::BigCharacter(item)
} else {
let item = MultilineTextChoiceItem::new(
String::from(choice),
ButtonLayout::default_three_icons(),
)
.use_delimiter(' ');
ChoiceItem::MultilineText(item)
};
// Action buttons have different middle button text
if [EXIT_INDEX, DELETE_INDEX, SHOW_INDEX, PROMPT_INDEX].contains(&(choice_index as usize)) {
let confirm_btn = ButtonDetails::armed_text("CONFIRM");
choice_item.set_middle_btn(Some(confirm_btn));
}
// Changing the prompt text for the wanted one
if choice_index == PROMPT_INDEX as u8 {
choice_item.set_text(String::from(self.prompt.as_ref()));
}
choice_item
}
fn count(&self) -> u8 {
CHOICE_LENGTH as u8
}
}
/// Component for entering a PIN.
pub struct PinEntry {
choice_page: ChoicePage<ChoiceFactoryPIN>,
pin_dots: Child<ChangingTextLine<String<MAX_PIN_LENGTH>>>,
show_real_pin: bool,
textbox: TextBox<MAX_PIN_LENGTH>,
}
impl PinEntry {
pub fn new(prompt: StrBuffer) -> Self {
let choices = ChoiceFactoryPIN::new(prompt);
Self {
choice_page: ChoicePage::new(choices)
.with_initial_page_counter(3)
.with_carousel(),
pin_dots: Child::new(ChangingTextLine::center_mono(String::new())),
show_real_pin: false,
textbox: TextBox::empty(),
}
}
fn append_new_digit(&mut self, ctx: &mut EventCtx, page_counter: u8) {
let digit = CHOICES[page_counter as usize];
self.textbox.append_slice(ctx, digit);
}
fn delete_last_digit(&mut self, ctx: &mut EventCtx) {
self.textbox.delete_last(ctx);
}
fn update_pin_dots(&mut self, ctx: &mut EventCtx) {
// TODO: this is the same action as for the passphrase entry,
// might do a common component that will handle this part,
// (something like `SecretTextLine`)
// also with things like shifting the dots when too many etc.
// TODO: when the PIN is longer than fits the screen, we might show ellipsis
if self.show_real_pin {
let pin = String::from(self.pin());
self.pin_dots.inner_mut().update_text(pin);
} else {
let mut dots: String<MAX_PIN_LENGTH> = String::new();
for _ in 0..self.textbox.len() {
unwrap!(dots.push_str("*"));
}
self.pin_dots.inner_mut().update_text(dots);
}
self.pin_dots.request_complete_repaint(ctx);
}
pub fn pin(&self) -> &str {
self.textbox.content()
}
fn is_full(&self) -> bool {
self.textbox.is_full()
}
fn is_empty(&self) -> bool {
self.textbox.is_empty()
}
}
impl Component for PinEntry {
type Msg = PinEntryMsg;
fn place(&mut self, bounds: Rect) -> Rect {
let pin_area_height = self.pin_dots.inner().needed_height();
let (pin_area, choice_area) = bounds.split_top(pin_area_height);
self.pin_dots.place(pin_area);
self.choice_page.place(choice_area);
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
// Any event when showing real PIN should hide it
if self.show_real_pin {
self.show_real_pin = false;
self.update_pin_dots(ctx);
}
let msg = self.choice_page.event(ctx, event);
if let Some(ChoicePageMsg::Choice(page_counter)) = msg {
// Performing action under specific index or appending new digit
match page_counter as usize {
EXIT_INDEX => return Some(PinEntryMsg::Cancelled),
DELETE_INDEX => {
self.delete_last_digit(ctx);
self.update_pin_dots(ctx);
ctx.request_paint();
}
SHOW_INDEX => {
self.show_real_pin = true;
self.update_pin_dots(ctx);
ctx.request_paint();
}
PROMPT_INDEX => return Some(PinEntryMsg::Confirmed),
_ => {
if !self.is_full() {
self.append_new_digit(ctx, page_counter);
self.update_pin_dots(ctx);
// Choosing any random digit to be shown next
let new_page_counter =
random::uniform_between(4, (CHOICE_LENGTH - 1) as u32);
self.choice_page
.set_page_counter(ctx, new_page_counter as u8);
ctx.request_paint();
}
}
}
}
None
}
fn paint(&mut self) {
self.pin_dots.paint();
self.choice_page.paint();
}
}
#[cfg(feature = "ui_debug")]
use super::{ButtonAction, ButtonPos};
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for PinEntry {
fn get_btn_action(&self, pos: ButtonPos) -> String<25> {
match pos {
ButtonPos::Left => ButtonAction::PrevPage.string(),
ButtonPos::Right => ButtonAction::NextPage.string(),
ButtonPos::Middle => {
let current_index = self.choice_page.page_index() as usize;
match current_index {
EXIT_INDEX => ButtonAction::Cancel.string(),
DELETE_INDEX => ButtonAction::Action("Delete last digit").string(),
SHOW_INDEX => ButtonAction::Action("Show PIN").string(),
PROMPT_INDEX => ButtonAction::Confirm.string(),
_ => ButtonAction::select_item(CHOICES[current_index]),
}
}
}
}
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("PinEntry");
// NOTE: `show_real_pin` was not able to be transferred,
// as it is true only for a very small amount of time
t.kw_pair("textbox", self.textbox.content());
self.report_btn_actions(t);
t.field("choice_page", &self.choice_page);
t.close();
}
}

View File

@ -15,6 +15,8 @@ use crate::{
},
};
use super::ButtonStyleSheet;
pub enum ResultPopupMsg {
Confirmed,
}
@ -48,10 +50,11 @@ impl ResultPopup {
.with_placement(LinearPlacement::vertical().align_at_center());
let button = button_text.map(|t| {
// TODO: use `ButtonController` for this
Child::new(Button::with_text(
ButtonPos::Right,
t,
theme::button_default(),
ButtonStyleSheet::default(true, false, None, None),
))
});

View File

@ -0,0 +1,165 @@
use crate::ui::{
component::{Component, Event, EventCtx, Never, Pad},
display,
geometry::{Offset, Point, Rect},
model_tr::theme,
};
/// In which direction should the scrollbar be positioned
pub enum ScrollbarOrientation {
Vertical,
Horizontal,
}
pub struct ScrollBar {
area: Rect,
pad: Pad,
pub page_count: usize,
pub active_page: usize,
pub orientation: ScrollbarOrientation,
}
impl ScrollBar {
pub const WIDTH: i16 = 8;
pub const DOT_SIZE: Offset = Offset::new(4, 4);
pub const DOT_INTERVAL: i16 = 6;
pub fn new(page_count: usize, orientation: ScrollbarOrientation) -> Self {
Self {
area: Rect::zero(),
pad: Pad::with_background(theme::BG),
page_count,
active_page: 0,
orientation,
}
}
/// Page count will be given later as it is not available yet.
pub fn vertical_to_be_filled_later() -> Self {
Self::vertical(0)
}
pub fn vertical(page_count: usize) -> Self {
Self::new(page_count, ScrollbarOrientation::Vertical)
}
pub fn horizontal(page_count: usize) -> Self {
Self::new(page_count, ScrollbarOrientation::Horizontal)
}
pub fn set_page_count(&mut self, page_count: usize) {
self.page_count = page_count;
}
pub fn set_active_page(&mut self, active_page: usize) {
self.active_page = active_page;
}
pub fn has_next_page(&self) -> bool {
self.active_page < self.page_count - 1
}
pub fn has_previous_page(&self) -> bool {
self.active_page > 0
}
pub fn go_to_next_page(&mut self) {
self.active_page = self.active_page.saturating_add(1).min(self.page_count - 1);
}
pub fn go_to_previous_page(&mut self) {
self.active_page = self.active_page.saturating_sub(1);
}
/// Create a (seemingly circular) dot given its top left point.
/// Make it full when it is active, otherwise paint just the perimeter and
/// leave center empty.
fn paint_dot(&self, active: bool, top_left: Point) {
let full_square = Rect::from_top_left_and_size(top_left, ScrollBar::DOT_SIZE);
// FG - painting the full square
display::rect_fill(full_square, theme::FG);
// BG - erase four corners
for p in full_square.corner_points().iter() {
display::paint_point(p, theme::BG);
}
// BG - erasing the middle when not active
if !active {
display::rect_fill(full_square.shrink(1), theme::BG)
}
}
fn paint_vertical(&mut self) {
let count = self.page_count as i16;
let interval = {
let available_space = self.area.height();
let naive_space = count * Self::DOT_INTERVAL;
if naive_space > available_space {
available_space / count
} else {
Self::DOT_INTERVAL
}
};
let mut top_left = Point::new(
self.area.center().x - Self::DOT_SIZE.x / 2,
self.area.center().y - (count / 2) * interval,
);
for i in 0..self.page_count {
self.paint_dot(i == self.active_page, top_left);
top_left.y += interval;
}
}
fn paint_horizontal(&mut self) {
let count = self.page_count as i16;
let interval = {
let available_space = self.area.width();
let naive_space = count * Self::DOT_INTERVAL;
if naive_space > available_space {
available_space / count
} else {
Self::DOT_INTERVAL
}
};
let mut top_left = Point::new(
self.area.center().x - (count / 2) * interval,
self.area.center().y - Self::DOT_SIZE.y / 2,
);
for i in 0..self.page_count {
self.paint_dot(i == self.active_page, top_left);
top_left.x += interval;
}
}
}
impl Component for ScrollBar {
type Msg = Never;
fn place(&mut self, bounds: Rect) -> Rect {
self.area = bounds;
self.pad.place(bounds);
bounds
}
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
None
}
/// Displaying one dot for each page.
fn paint(&mut self) {
// Not showing the scrollbar dot when there is only one page
if self.page_count <= 1 {
return;
}
self.pad.clear();
self.pad.paint();
if matches!(self.orientation, ScrollbarOrientation::Vertical) {
self.paint_vertical()
} else {
self.paint_horizontal()
}
}
}

View File

@ -0,0 +1,134 @@
use crate::ui::{
component::{Component, Event, EventCtx},
geometry::Rect,
};
use super::{ButtonLayout, ChoiceFactory, ChoiceItem, ChoicePage, ChoicePageMsg, TextChoiceItem};
use heapless::{String, Vec};
#[cfg(feature = "ui_debug")]
use super::{ButtonAction, ButtonPos};
pub enum SimpleChoiceMsg {
Result(String<50>),
}
struct ChoiceFactorySimple<T, const N: usize> {
choices: Vec<T, N>,
}
impl<T, const N: usize> ChoiceFactorySimple<T, N>
where
T: AsRef<str>,
{
fn new(choices: Vec<T, N>) -> Self {
Self { choices }
}
}
impl<T, const N: usize> ChoiceFactory for ChoiceFactorySimple<T, N>
where
T: AsRef<str>,
{
fn get(&self, choice_index: u8) -> ChoiceItem {
let text = &self.choices[choice_index as usize];
let text_item = TextChoiceItem::new(text, ButtonLayout::default_three_icons());
let mut choice_item = ChoiceItem::Text(text_item);
// Disabling prev/next buttons for the first/last choice.
if choice_index == 0 {
choice_item.set_left_btn(None);
} else if choice_index as usize == N - 1 {
choice_item.set_right_btn(None);
}
choice_item
}
fn count(&self) -> u8 {
N as u8
}
}
/// Simple wrapper around `ChoicePage` that allows for
/// inputting a list of values and receiving the chosen one.
pub struct SimpleChoice<T, const N: usize>
where
T: AsRef<str>,
T: Clone,
{
choices: Vec<T, N>,
choice_page: ChoicePage<ChoiceFactorySimple<T, N>>,
}
impl<T, const N: usize> SimpleChoice<T, N>
where
T: AsRef<str>,
T: Clone,
{
pub fn new(str_choices: Vec<T, N>) -> Self {
let choices = ChoiceFactorySimple::new(str_choices.clone());
Self {
choices: str_choices,
choice_page: ChoicePage::new(choices),
}
}
}
impl<T, const N: usize> Component for SimpleChoice<T, N>
where
T: AsRef<str>,
T: Clone,
{
type Msg = SimpleChoiceMsg;
fn place(&mut self, bounds: Rect) -> Rect {
self.choice_page.place(bounds)
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
let msg = self.choice_page.event(ctx, event);
match msg {
Some(ChoicePageMsg::Choice(page_counter)) => {
let result = String::from(self.choices[page_counter as usize].as_ref());
Some(SimpleChoiceMsg::Result(result))
}
_ => None,
}
}
fn paint(&mut self) {
self.choice_page.paint();
}
}
#[cfg(feature = "ui_debug")]
impl<T, const N: usize> crate::trace::Trace for SimpleChoice<T, N>
where
T: AsRef<str>,
T: Clone,
{
fn get_btn_action(&self, pos: ButtonPos) -> String<25> {
match pos {
ButtonPos::Left => match self.choice_page.has_previous_choice() {
true => ButtonAction::PrevPage.string(),
false => ButtonAction::empty(),
},
ButtonPos::Right => match self.choice_page.has_next_choice() {
true => ButtonAction::NextPage.string(),
false => ButtonAction::empty(),
},
ButtonPos::Middle => {
let current_index = self.choice_page.page_index() as usize;
ButtonAction::select_item(self.choices[current_index].as_ref())
}
}
}
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("SimpleChoice");
self.report_btn_actions(t);
t.field("choice_page", &self.choice_page);
t.close();
}
}

View File

@ -1,30 +1,47 @@
use core::convert::TryInto;
use heapless::{String, Vec};
use crate::{
error::Error,
micropython::{buffer::StrBuffer, map::Map, module::Module, obj::Obj, qstr::Qstr, util},
micropython::{
buffer::StrBuffer,
iter::{Iter, IterBuf},
map::Map,
module::Module,
obj::Obj,
qstr::Qstr,
util,
},
time::Duration,
ui::{
component::{
base::Component,
base::{Component, ComponentExt},
paginated::{PageMsg, Paginate},
text::paragraphs::{Paragraph, Paragraphs},
FormattedText,
},
layout::{
obj::{ComponentMsgObj, LayoutObj},
result::{CANCELLED, CONFIRMED, INFO},
result::{CANCELLED, CONFIRMED, INFO}, util::iter_into_vec,
},
},
};
use super::{
component::{Button, ButtonPage, ButtonPos, Frame},
component::{
Bip39Entry, Bip39EntryMsg, ButtonActions, ButtonDetails, ButtonLayout, ButtonPage, Flow,
FlowMsg, FlowPages, Frame, Page, PassphraseEntry, PassphraseEntryMsg, PinEntry,
PinEntryMsg, SimpleChoice, SimpleChoiceMsg,
},
theme,
};
impl<T> ComponentMsgObj for ButtonPage<T>
impl<S, T> ComponentMsgObj for ButtonPage<S, T>
where
T: Component + Paginate,
S: AsRef<str>,
S: Clone,
{
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
@ -35,6 +52,56 @@ where
}
}
impl<F, const M: usize> ComponentMsgObj for Flow<F, M>
where
F: Fn(u8) -> Page<M>,
{
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
FlowMsg::Confirmed => Ok(CONFIRMED.as_obj()),
FlowMsg::Cancelled => Ok(CANCELLED.as_obj()),
}
}
}
impl ComponentMsgObj for PinEntry {
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
PinEntryMsg::Confirmed => self.pin().try_into(),
PinEntryMsg::Cancelled => Ok(CANCELLED.as_obj()),
}
}
}
impl<T, const N: usize> ComponentMsgObj for SimpleChoice<T, N>
where
T: AsRef<str>,
T: Clone,
{
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
SimpleChoiceMsg::Result(choice) => choice.as_str().try_into(),
}
}
}
impl ComponentMsgObj for Bip39Entry {
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
Bip39EntryMsg::ResultWord(word) => word.as_str().try_into(),
}
}
}
impl ComponentMsgObj for PassphraseEntry {
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
PassphraseEntryMsg::Confirmed => self.passphrase().try_into(),
PassphraseEntryMsg::Cancelled => Ok(CANCELLED.as_obj()),
}
}
}
impl<T, U> ComponentMsgObj for Frame<T, U>
where
T: ComponentMsgObj,
@ -55,28 +122,53 @@ extern "C" fn new_confirm_action(n_args: usize, args: *const Obj, kwargs: *mut M
let verb_cancel: Option<StrBuffer> =
kwargs.get(Qstr::MP_QSTR_verb_cancel)?.try_into_option()?;
let reverse: bool = kwargs.get(Qstr::MP_QSTR_reverse)?.try_into()?;
let hold: bool = kwargs.get(Qstr::MP_QSTR_hold)?.try_into()?;
// TODO: could be replaced by Flow with one element after it supports pagination
let format = match (&action, &description, reverse) {
(Some(_), Some(_), false) => "{bold}{action}\n\r{normal}{description}",
(Some(_), Some(_), true) => "{normal}{description}\n\r{bold}{action}",
(Some(_), None, _) => "{bold}{action}",
(None, Some(_), _) => "{normal}{description}",
(Some(_), Some(_), false) => "{Font::bold}{action}\n\r{Font::normal}{description}",
(Some(_), Some(_), true) => "{Font::normal}{description}\n\r{Font::bold}{action}",
(Some(_), None, _) => "{Font::bold}{action}",
(None, Some(_), _) => "{Font::normal}{description}",
_ => "",
};
let _left = verb_cancel
.map(|label| Button::with_text(ButtonPos::Left, label, theme::button_cancel()));
let _right =
verb.map(|label| Button::with_text(ButtonPos::Right, label, theme::button_default()));
let verb_cancel = verb_cancel.unwrap_or_default();
let verb = verb.unwrap_or_default();
let cancel_btn = if verb_cancel.len() > 0 {
Some(ButtonDetails::cancel_icon())
} else {
None
};
let mut confirm_btn = if verb.len() > 0 {
Some(ButtonDetails::text(verb))
} else {
None
};
// Optional HoldToConfirm
if hold {
// TODO: clients might want to set the duration
confirm_btn = confirm_btn.map(|btn| btn.with_duration(Duration::from_secs(2)));
}
// TODO: make sure the text will not be colliding with the buttons
// - make there some space on the bottom of the text
let obj = LayoutObj::new(Frame::new(
title,
ButtonPage::new(
None,
ButtonPage::new_str_buf(
FormattedText::new(theme::TEXT_NORMAL, theme::FORMATTED, format)
.with("action", action.unwrap_or_default())
.with("description", description.unwrap_or_default()),
theme::BG,
),
)
.with_cancel_btn(cancel_btn)
.with_confirm_btn(confirm_btn),
))?;
Ok(obj.into())
};
@ -90,9 +182,12 @@ extern "C" fn new_confirm_text(n_args: usize, args: *const Obj, kwargs: *mut Map
let description: Option<StrBuffer> =
kwargs.get(Qstr::MP_QSTR_description)?.try_into_option()?;
// TODO: could be replaced by Flow with one element after it supports pagination
let obj = LayoutObj::new(Frame::new(
title,
ButtonPage::new(
None,
ButtonPage::new_str(
Paragraphs::new([
Paragraph::new(&theme::TEXT_NORMAL, description.unwrap_or_default()),
Paragraph::new(&theme::TEXT_BOLD, data),
@ -105,6 +200,401 @@ extern "C" fn new_confirm_text(n_args: usize, args: *const Obj, kwargs: *mut Map
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn confirm_output(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = |_args: &[Obj], kwargs: &Map| {
let address: StrBuffer = kwargs.get(Qstr::MP_QSTR_address)?.try_into()?;
// Getting this from micropython so it is also a `StrBuffer`, not having
// to handle the string operation in Rust, which would make it a `String`
// (which would them cause issues with general `T: AsRef<str>` parameter)
let truncated_address: StrBuffer =
kwargs.get(Qstr::MP_QSTR_truncated_address)?.try_into()?;
let amount: StrBuffer = kwargs.get(Qstr::MP_QSTR_amount)?.try_into()?;
let title: StrBuffer = "Send".into();
let get_page = move |page_index| {
// Showing two screens - the recipient address and summary confirmation
match page_index {
0 => {
// `icon + label + address`
let btn_layout = ButtonLayout::new(
Some(ButtonDetails::cancel_icon()),
None,
Some(ButtonDetails::text("CONTINUE")),
);
let btn_actions = ButtonActions::cancel_next();
Page::<20>::new(btn_layout, btn_actions).icon_label_text(
theme::ICON_USER,
"Recipient".into(),
address.clone(),
)
}
1 => {
// 2 pairs `icon + label + text`
let btn_layout = ButtonLayout::new(
Some(ButtonDetails::cancel_icon()),
None,
Some(
ButtonDetails::text("HOLD TO CONFIRM")
.with_duration(Duration::from_secs(2)),
),
);
let btn_actions = ButtonActions::cancel_confirm();
Page::<20>::new(btn_layout, btn_actions)
.icon_label_text(
theme::ICON_USER,
"Recipient".into(),
truncated_address.clone(),
)
.newline()
.icon_label_text(theme::ICON_AMOUNT, "Amount".into(), amount.clone())
}
_ => unreachable!(),
}
};
let pages = FlowPages::new(get_page, 2);
let obj = LayoutObj::new(Flow::new(pages).with_common_title(title).into_child())?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn confirm_total(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let total_amount: StrBuffer = kwargs.get(Qstr::MP_QSTR_total_amount)?.try_into()?;
let fee_amount: StrBuffer = kwargs.get(Qstr::MP_QSTR_fee_amount)?.try_into()?;
let fee_rate_amount: Option<StrBuffer> = kwargs
.get(Qstr::MP_QSTR_fee_rate_amount)?
.try_into_option()?;
let total_label: StrBuffer = kwargs.get(Qstr::MP_QSTR_total_label)?.try_into()?;
let fee_label: StrBuffer = kwargs.get(Qstr::MP_QSTR_fee_label)?.try_into()?;
let get_page = move |page_index| {
// One page with 2 or 3 pairs `icon + label + text`
assert!(page_index == 0);
let btn_layout = ButtonLayout::new(
Some(ButtonDetails::cancel_icon()),
None,
Some(ButtonDetails::text("HOLD TO SEND").with_duration(Duration::from_secs(2))),
);
let btn_actions = ButtonActions::cancel_confirm();
let mut flow_page = Page::<25>::new(btn_layout, btn_actions)
.icon_label_text(theme::ICON_PARAM, total_label.clone(), total_amount.clone())
.newline()
.icon_label_text(theme::ICON_PARAM, fee_label.clone(), fee_amount.clone());
if let Some(fee_rate_amount) = &fee_rate_amount {
flow_page = flow_page.newline().icon_label_text(
theme::ICON_PARAM,
"Fee rate".into(),
fee_rate_amount.clone(),
)
}
flow_page
};
let pages = FlowPages::new(get_page, 1);
let obj = LayoutObj::new(Flow::new(pages).with_common_title(title).into_child())?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn tutorial(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = |_args: &[Obj], _kwargs: &Map| {
let get_page = |page_index| {
// Lazy-loaded list of screens to show, with custom content,
// buttons and actions triggered by these buttons.
// Cancelling the first screen will point to the last one,
// which asks for confirmation whether user wants to
// really cancel the tutorial.
let screen = match page_index {
// title, text, btn_layout, btn_actions
0 => (
"Hello!",
"Welcome to Trezor.\n\n\nPress right to continue.",
ButtonLayout::cancel_and_arrow(),
ButtonActions::last_next(),
),
1 => (
"Basics",
"Use Trezor by clicking left & right.\nPress right to continue.",
ButtonLayout::left_right_arrows(),
ButtonActions::prev_next(),
),
2 => (
"Confirm",
"Press both left & right at the same time to confirm.",
ButtonLayout::new(
Some(ButtonDetails::left_arrow_icon()),
Some(ButtonDetails::armed_text("CONFIRM")),
None,
),
ButtonActions::prev_next_with_middle(),
),
3 => (
"Hold to confirm",
"Press & hold right to approve important operations.",
ButtonLayout::new(
Some(ButtonDetails::left_arrow_icon()),
None,
Some(
ButtonDetails::text("HOLD TO CONFIRM")
.with_duration(Duration::from_millis(2000)),
),
),
ButtonActions::prev_next(),
),
// TODO: merge these two scrolls into one, with using a scrollbar
4 => (
"Screen scroll",
"Press right to scroll down to read all content when text doesn't...",
ButtonLayout::new(
Some(ButtonDetails::left_arrow_icon()),
None,
Some(ButtonDetails::down_arrow_icon_wide()),
),
ButtonActions::prev_next(),
),
5 => (
"Screen scroll",
"fit on one screen. Press left to scroll up.",
ButtonLayout::new(
Some(ButtonDetails::up_arrow_icon_wide()),
None,
Some(ButtonDetails::text("CONFIRM")),
),
ButtonActions::prev_next(),
),
6 => (
"Congrats!",
"You're ready to use Trezor.",
ButtonLayout::new(
Some(ButtonDetails::text("AGAIN")),
None,
Some(ButtonDetails::text("FINISH")),
),
ButtonActions::beginning_confirm(),
),
7 => (
"Skip tutorial?",
"Sure you want to skip the tutorial?",
ButtonLayout::new(
Some(ButtonDetails::left_arrow_icon()),
None,
Some(ButtonDetails::text("CONFIRM")),
),
ButtonActions::beginning_cancel(),
),
_ => unreachable!(),
};
Page::<10>::new(screen.2.clone(), screen.3.clone())
.text_bold(screen.0.into())
.newline()
.newline_half()
.text_normal(screen.1.into())
};
let pages = FlowPages::new(get_page, 8);
let obj = LayoutObj::new(Flow::new(pages).into_child())?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn pin_confirm_action(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = |_args: &[Obj], kwargs: &Map| {
let action: StrBuffer = kwargs.get(Qstr::MP_QSTR_action)?.try_into()?;
let get_page = move |page_index| {
let screen = match page_index {
// title, text, btn_layout, btn_actions
// NOTE: doing the newlines manually to look exactly same
// as in the design.
0 => (
"PIN settings".into(),
"PIN should\ncontain at\nleast four\ndigits",
ButtonLayout::cancel_and_text("GOT IT"),
ButtonActions::cancel_next(),
),
1 => (
action.clone(),
"You'll use\nthis PIN to\naccess this\ndevice.",
ButtonLayout::cancel_and_htc_text(
"HOLD TO CONFIRM",
Duration::from_millis(1000),
),
ButtonActions::cancel_confirm(),
),
_ => unreachable!(),
};
Page::<10>::new(screen.2.clone(), screen.3.clone())
.text_bold(screen.0)
.newline()
.newline_half()
.text_normal(screen.1.into())
};
let pages = FlowPages::new(get_page, 2);
let obj = LayoutObj::new(Flow::new(pages).into_child())?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn request_pin(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = |_args: &[Obj], kwargs: &Map| {
let prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?;
let _subprompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_subprompt)?.try_into()?;
let _allow_cancel: Option<bool> =
kwargs.get(Qstr::MP_QSTR_allow_cancel)?.try_into_option()?;
let obj = LayoutObj::new(PinEntry::new(prompt).into_child())?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn show_share_words(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = |_args: &[Obj], kwargs: &Map| {
let share_words_obj: Obj = kwargs.get(Qstr::MP_QSTR_share_words)?;
let title = "Recovery seed";
// Parsing the list of share words.
// Assume there is always up to 24 words in the newly generated seed
// (for now, later we might support SLIP39 with up to 33 words)
let mut iter_buf = IterBuf::new();
let iter_words = Iter::try_from_obj_with_buf(share_words_obj, &mut iter_buf)?;
let mut share_words: Vec<StrBuffer, 24> = Vec::new();
for word in iter_words {
share_words.push(word.try_into()?).unwrap();
}
let share_words_len = share_words.len() as u8;
let beginning_text = build_string!(
40,
"Write down these ",
inttostr!(share_words_len),
" words:\n\n"
);
let mut middle_words: String<360> = String::new();
// Vec<StrBuffer> does not support `enumerate()`
let mut index: u8 = 0;
for word in share_words {
index += 1;
let line = build_string!(15, inttostr!(index), ". ", word.as_ref(), "\n");
middle_words.push_str(&line).unwrap();
}
let end_text = build_string!(
40,
"I wrote down all ",
inttostr!(share_words_len),
" words in order."
);
// TODO: instead of this could create a new paragraph for the beginning,
// each word and the end
let text_to_show = build_string!(
440,
beginning_text.as_str(),
middle_words.as_str(),
end_text.as_str()
);
// Adding hold-to-confirm button at the end
// Also no possibility of cancelling
let cancel_btn = None;
let confirm_btn =
Some(ButtonDetails::text("CONFIRM").with_duration(Duration::from_secs(2)));
let obj = LayoutObj::new(Frame::new(
title,
None,
ButtonPage::new_str(
Paragraphs::new(
[
Paragraph::new(&theme::TEXT_BOLD, text_to_show)
]
),
theme::BG,
)
.with_cancel_btn(cancel_btn)
.with_confirm_btn(confirm_btn),
))?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn select_word(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let description: StrBuffer = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?;
let words_iterable: Obj = kwargs.get(Qstr::MP_QSTR_words)?;
let words: Vec<StrBuffer, 3> = iter_into_vec(words_iterable)?;
let obj = LayoutObj::new(Frame::new(
title,
Some(description),
SimpleChoice::new(words).into_child(),
))?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn request_word_count(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let text: StrBuffer = kwargs.get(Qstr::MP_QSTR_text)?.try_into()?;
let choices: Vec<&str, 5> = ["12", "18", "20", "24", "33"].into_iter().collect();
let obj = LayoutObj::new(Frame::new(
title,
Some(text),
SimpleChoice::new(choices).into_child(),
))?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn request_word_bip39(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = |_args: &[Obj], kwargs: &Map| {
let prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?;
let obj = LayoutObj::new(Frame::new(prompt, None, Bip39Entry::new().into_child()))?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn request_passphrase(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = |_args: &[Obj], kwargs: &Map| {
let prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?;
let _max_len: u8 = kwargs.get(Qstr::MP_QSTR_max_len)?.try_into()?;
let obj = LayoutObj::new(Frame::new(
prompt,
None,
PassphraseEntry::new().into_child(),
))?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
#[no_mangle]
pub static mp_module_trezorui2: Module = obj_module! {
Qstr::MP_QSTR___name__ => Qstr::MP_QSTR_trezorui2.to_obj(),
@ -125,12 +615,50 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// description: str | None = None,
/// verb: str | None = None,
/// verb_cancel: str | None = None,
/// hold: bool | None = None,
/// hold: bool = False,
/// reverse: bool = False,
/// ) -> object:
/// """Confirm action."""
Qstr::MP_QSTR_confirm_action => obj_fn_kw!(0, new_confirm_action).as_obj(),
/// def confirm_output_r(
/// *,
/// address: str,
/// truncated_address: str,
/// amount: str,
/// ) -> object:
/// """Confirm output. Specific for model R."""
Qstr::MP_QSTR_confirm_output_r => obj_fn_kw!(0, confirm_output).as_obj(),
/// def confirm_total_r(
/// *,
/// title: str,
/// total_amount: str,
/// fee_amount: str,
/// fee_rate_amount: str | None = None,
/// total_label: str,
/// fee_label: str,
/// ) -> object:
/// """Confirm summary of a transaction. Specific for model R."""
Qstr::MP_QSTR_confirm_total_r => obj_fn_kw!(0, confirm_total).as_obj(),
/// def tutorial() -> object:
/// """Show user how to interact with the device."""
Qstr::MP_QSTR_tutorial => obj_fn_kw!(0, tutorial).as_obj(),
/// def pin_confirm_action(*, action: str) -> object:
/// """Confirm PIN action and informing user about it."""
Qstr::MP_QSTR_pin_confirm_action => obj_fn_kw!(0, pin_confirm_action).as_obj(),
/// def request_pin(
/// *,
/// prompt: str,
/// subprompt: str | None = None,
/// allow_cancel: bool | None = None,
/// ) -> str | object:
/// """Request pin on device."""
Qstr::MP_QSTR_request_pin => obj_fn_kw!(0, request_pin).as_obj(),
/// def confirm_text(
/// *,
/// title: str,
@ -139,103 +667,143 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// ) -> object:
/// """Confirm text."""
Qstr::MP_QSTR_confirm_text => obj_fn_kw!(0, new_confirm_text).as_obj(),
/// def show_share_words(
/// *,
/// share_words: Iterable[str],
/// ) -> None:
/// """Shows a backup seed."""
Qstr::MP_QSTR_show_share_words => obj_fn_kw!(0, show_share_words).as_obj(),
/// def select_word(
/// *,
/// title: str,
/// description: str,
/// words: Iterable[str],
/// ) -> int:
/// """Select mnemonic word from three possibilities - seed check after backup. The
/// iterable must be of exact size. Returns index in range `0..3`."""
Qstr::MP_QSTR_select_word => obj_fn_kw!(0, select_word).as_obj(),
/// def request_word_count(
/// *,
/// title: str,
/// text: str,
/// ) -> str: # TODO: make it return int
/// """Get word count for recovery."""
Qstr::MP_QSTR_request_word_count => obj_fn_kw!(0, request_word_count).as_obj(),
/// def request_word_bip39(
/// *,
/// prompt: str,
/// ) -> str:
/// """Get recovery word for BIP39."""
Qstr::MP_QSTR_request_word_bip39 => obj_fn_kw!(0, request_word_bip39).as_obj(),
/// def request_passphrase(
/// *,
/// prompt: str,
/// max_len: int,
/// ) -> str:
/// """Get passphrase."""
Qstr::MP_QSTR_request_passphrase => obj_fn_kw!(0, request_passphrase).as_obj(),
};
#[cfg(test)]
mod tests {
use crate::{
trace::Trace,
ui::{
component::Component,
model_tr::{
component::{Dialog, DialogMsg},
constant,
},
},
};
// use crate::{
// trace::Trace,
// ui::{
// component::Component,
// model_tr::{
// component::{Dialog, DialogMsg},
// constant,
// },
// },
// };
use super::*;
// use super::*;
fn trace(val: &impl Trace) -> String {
let mut t = Vec::new();
val.trace(&mut t);
String::from_utf8(t).unwrap()
}
// fn trace(val: &impl Trace) -> String {
// let mut t = Vec::new();
// val.trace(&mut t);
// String::from_utf8(t).unwrap()
// }
impl<T, U> ComponentMsgObj for Dialog<T, U>
where
T: ComponentMsgObj,
U: AsRef<str>,
{
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
DialogMsg::Content(c) => self.inner().msg_try_into_obj(c),
DialogMsg::LeftClicked => Ok(CANCELLED.as_obj()),
DialogMsg::RightClicked => Ok(CONFIRMED.as_obj()),
}
}
}
// impl<T, U> ComponentMsgObj for Dialog<T, U>
// where
// T: ComponentMsgObj,
// U: AsRef<str>,
// {
// fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error>
// { match msg {
// DialogMsg::Content(c) =>
// self.inner().msg_try_into_obj(c),
// DialogMsg::LeftClicked => Ok(CANCELLED.as_obj()),
// DialogMsg::RightClicked => Ok(CONFIRMED.as_obj()), }
// }
// }
#[test]
fn trace_example_layout() {
let mut layout = Dialog::new(
FormattedText::new(
theme::TEXT_NORMAL,
theme::FORMATTED,
"Testing text layout, with some text, and some more text. And {param}",
)
.with("param", "parameters!"),
Some(Button::with_text(
ButtonPos::Left,
"Left",
theme::button_cancel(),
)),
Some(Button::with_text(
ButtonPos::Right,
"Right",
theme::button_default(),
)),
);
layout.place(constant::screen());
assert_eq!(
trace(&layout),
r#"<Dialog content:<Text content:Testing text layout,
with some text, and
some more text. And p-
arameters! > left:<Button text:Left > right:<Button text:Right > >"#
)
}
// #[test]
// fn trace_example_layout() {
// let mut layout = Dialog::new(
// FormattedText::new(
// theme::TEXT_NORMAL,
// theme::FORMATTED,
// "Testing text layout, with some text, and some more text.
// And {param}", )
// .with("param", "parameters!"),
// Some(Button::with_text(
// ButtonPos::Left,
// "Left",
// theme::button_cancel(),
// )),
// Some(Button::with_text(
// ButtonPos::Right,
// "Right",
// theme::button_default(),
// )),
// );
// layout.place(constant::screen());
// assert_eq!(
// trace(&layout),
// r#"<Dialog content:<Text content:Testing text layout,
// with some text, and
// some more text. And p-
// arameters! > left:<Button text:Left > right:<Button text:Right > >"#
// )
// }
#[test]
fn trace_layout_title() {
let mut layout = Frame::new(
"Please confirm",
Dialog::new(
FormattedText::new(
theme::TEXT_NORMAL,
theme::FORMATTED,
"Testing text layout, with some text, and some more text. And {param}",
)
.with("param", "parameters!"),
Some(Button::with_text(
ButtonPos::Left,
"Left",
theme::button_cancel(),
)),
Some(Button::with_text(
ButtonPos::Right,
"Right",
theme::button_default(),
)),
),
);
layout.place(constant::screen());
assert_eq!(
trace(&layout),
r#"<Frame title:Please confirm content:<Dialog content:<Text content:Testing text layout,
with some text, and
some more text. And p-
arameters! > left:<Button text:Left > right:<Button text:Right > > >"#
)
}
// #[test]
// fn trace_layout_title() {
// let mut layout = Frame::new(
// "Please confirm",
// Dialog::new(
// FormattedText::new(
// theme::TEXT_NORMAL,
// theme::FORMATTED,
// "Testing text layout, with some text, and some more
// text. And {param}", )
// .with("param", "parameters!"),
// Some(Button::with_text(
// ButtonPos::Left,
// "Left",
// theme::button_cancel(),
// )),
// Some(Button::with_text(
// ButtonPos::Right,
// "Right",
// theme::button_default(),
// )),
// ),
// );
// layout.place(constant::screen());
// assert_eq!(
// trace(&layout),
// r#"<Frame title:Please confirm content:<Dialog content:<Text
// content:Testing text layout, with some text, and
// some more text. And p-
// arameters! > left:<Button text:Left > right:<Button text:Right > > >"#
// )
// }
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More