WIP - fixes after rebase

tychovrahe/rust_bootloader_/tr
grdddj 2 years ago
parent 681fc6c398
commit 0da459e30a

@ -149,6 +149,39 @@ core fw btconly t1 build:
- firmware-T1B1-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:
@ -263,6 +296,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:

@ -246,6 +246,25 @@ ui tests 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:

@ -10,11 +10,11 @@ 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"
model_file_hashes = {k: v for k, v in FILE_HASHES.items() if k.startswith(f"{model}_")}

@ -119,7 +119,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

@ -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
}
/**

@ -316,6 +316,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
}
/**
@ -358,6 +359,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

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

@ -23,6 +23,7 @@ PYOPT ?= 1
BITCOIN_ONLY ?= 0
TREZOR_MODEL ?= T
TREZOR_MEMPERF ?= 0
TREZOR_EMULATOR_FROZEN ?= 0
ADDRESS_SANITIZER ?= 0
# OpenOCD interface default. Alternative: ftdi/olimex-arm-usb-tiny-h
@ -83,7 +84,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)
@ -181,7 +182,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)" 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 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 TREZOR_EMULATOR_DEBUGGABLE=1
build_cross: ## build mpy-cross port
$(MAKE) -C vendor/micropython/mpy-cross $(CROSS_PORT_OPTS)

@ -590,8 +590,13 @@ if FROZEN:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/recovery.py'))
if EVERYTHING:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/fido.py'))
elif TREZOR_MODEL in ('1', 'R'):
elif TREZOR_MODEL in ('R'):
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tr/__init__.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tr/homescreen.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/fido.py'))
else:
raise ValueError('Unknown Trezor model')
@ -620,7 +625,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',

@ -542,13 +542,13 @@ if FROZEN:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/recovery.py'))
if EVERYTHING:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/fido.py'))
elif TREZOR_MODEL in ('1', 'R'):
elif TREZOR_MODEL in ('R'):
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tr/__init__.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tr/homescreen.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'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tr/fido.py'))
else:
raise ValueError('Unknown Trezor model')
@ -577,7 +577,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

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

@ -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)},

@ -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,
};

@ -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[];

@ -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,
};

@ -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[];

@ -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 };

@ -19,6 +19,8 @@ static void _librust_qstrs(void) {
MP_QSTR_disable_animation;
MP_QSTR_jpeg_info;
MP_QSTR_jpeg_test;
MP_QSTR_checked_index;
MP_QSTR_choices;
MP_QSTR_confirm_action;
MP_QSTR_confirm_homescreen;
MP_QSTR_confirm_blob;
@ -28,8 +30,14 @@ 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_more;
@ -44,7 +52,12 @@ static void _librust_qstrs(void) {
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;
@ -52,7 +65,16 @@ static void _librust_qstrs(void) {
MP_QSTR_show_group_share_success;
MP_QSTR_show_homescreen;
MP_QSTR_show_lockscreen;
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_show_progress;
@ -65,8 +87,20 @@ static void _librust_qstrs(void) {
MP_QSTR_paint;
MP_QSTR_request_complete_repaint;
MP_QSTR_trace;
MP_QSTR_request_word_count;
MP_QSTR_request_word_bip39;
MP_QSTR_tutorial;
MP_QSTR_attach_timer_fn;
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;
@ -113,4 +147,9 @@ static void _librust_qstrs(void) {
MP_QSTR_bootscreen;
MP_QSTR_skip_first_paint;
MP_QSTR_wrong_pin;
MP_QSTR_fee_amount;
MP_QSTR_fee_rate_amount;
MP_QSTR_total_label;
MP_QSTR_fee_label;
MP_QSTR_truncated_address;
}

@ -20,6 +20,7 @@ use super::ffi;
/// The `off` field represents offset from the `ptr` and allows us to do
/// substring slices while keeping the head pointer as required by GC.
#[repr(C)]
#[derive(Debug, Clone)]
pub struct StrBuffer {
ptr: *const u8,
len: u16,

@ -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(">")
}
}
}

@ -52,6 +52,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 {
@ -198,4 +217,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);
}
}

@ -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)));
}
}
}

@ -84,6 +84,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

@ -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;
}

@ -18,7 +18,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);
}
@ -31,6 +33,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 {
@ -61,6 +68,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 {

@ -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),
}
}
}

@ -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();
}
}
}

@ -118,18 +118,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>>,
@ -141,12 +151,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, ..
@ -173,6 +187,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,
@ -180,11 +197,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,
@ -208,7 +224,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..];
@ -224,7 +245,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.
@ -264,6 +285,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()
@ -282,6 +304,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 },
@ -290,6 +314,7 @@ pub enum LayoutFit {
}
impl LayoutFit {
/// How high is the processed/fitted content.
pub fn height(&self) -> i16 {
match self {
LayoutFit::Fitting { height, .. } => *height,
@ -298,19 +323,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 {
@ -351,6 +390,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> {
@ -372,6 +412,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.
@ -383,6 +424,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,
@ -404,19 +449,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 {
@ -480,7 +527,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;
@ -501,7 +548,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),

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

@ -231,6 +231,7 @@ 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();
Self::foreach_visible(
&self.source,
&self.visible,
@ -240,6 +241,7 @@ pub mod trace {
t.string("\n");
},
);
t.content_flag();
t.close();
}
}

@ -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());
}
}

@ -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()
}
}

@ -0,0 +1,183 @@
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
}
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, FromPrimitive)]
#[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())
}
pub fn char_width(self, ch: char) -> i16 {
display::char_width(ch, self.into())
}
pub fn text_height(self) -> i16 {
display::text_height(self.into())
}
pub fn text_max_height(self) -> i16 {
display::text_max_height(self.into())
}
pub fn text_baseline(self) -> i16 {
display::text_baseline(self.into())
}
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
}
}

@ -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);
}
}

@ -2,6 +2,9 @@
pub mod loader;
#[cfg(feature = "jpeg")]
pub mod tjpgd;
pub mod icon;
pub mod font;
pub mod color;
use super::{
constant,
@ -26,10 +29,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)
@ -57,6 +63,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());
}
@ -68,17 +75,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
@ -96,9 +108,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,
@ -181,35 +199,61 @@ pub(crate) 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);
@ -228,7 +272,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),
@ -824,9 +873,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,
@ -869,6 +918,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)
}
@ -940,252 +1002,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, FromPrimitive)]
#[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())
}
pub fn char_width(self, ch: char) -> i16 {
display::char_width(ch, self.into())
}
pub fn text_height(self) -> i16 {
display::text_height(self.into())
}
pub fn text_max_height(self) -> i16 {
display::text_max_height(self.into())
}
pub fn text_baseline(self) -> i16 {
display::text_baseline(self.into())
}
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()
}
}

@ -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 {

@ -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 {
let x0 = p.x - size.x / 2;
let y0 = p.y - size.y / 2;
@ -298,10 +313,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),
@ -311,6 +336,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,
@ -320,6 +347,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,
@ -329,6 +362,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,
@ -338,6 +372,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());
@ -352,10 +407,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());
@ -370,10 +427,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),
@ -391,6 +459,7 @@ impl Rect {
}
}
/// Moving `Rect` by the given offset.
pub const fn translate(&self, offset: Offset) -> Self {
Self {
x0: self.x0 + offset.x,
@ -399,6 +468,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)]
@ -513,7 +592,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;

@ -204,6 +204,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.
@ -227,30 +235,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(">")
}
}
@ -293,6 +322,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(),
@ -477,6 +507,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()?;

@ -40,6 +40,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>,
{
@ -49,8 +59,7 @@ 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)
}
/// Maximum number of characters that can be displayed on screen at once. Used

@ -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"
}
}};
}

@ -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;

@ -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)
}

@ -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();
}
}

@ -59,10 +59,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}",
_ => "",
};

@ -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.

@ -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();
}
}

@ -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
};
// 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()
}
}
};
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,
};
let start_of_baseline = area.bottom_left() + Offset::new(border_width, -2);
// Allowing for possible offset of the area from current style
if let Some(offset) = style.offset {
area.translate(offset)
} else {
area
}
}
(area, start_of_baseline)
/// 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();
}
}

@ -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();
}
}

@ -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(),
}
}
}

@ -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();
}
}

@ -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();
}
}

@ -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
}

@ -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);

@ -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));

@ -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()
}
}

@ -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();
}
}

@ -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,
}
}
}

@ -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();
}

@ -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();

@ -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};

@ -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<&'static str, T>
where
T: Paginate,
T: Component,
{
/// Constructor for `&'static str` button-text type.
pub fn new_str(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")),
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<T> ButtonPage<T>
impl<T> ButtonPage<StrBuffer, T>
where
T: Paginate,
T: Component,
{
pub fn new(content: T, background: Color) -> Self {
/// Constructor for `StrBuffer` button-text type.
pub fn new_str_buf(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".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));
}
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);
/// 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<T> Component for ButtonPage<T>
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 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.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,
)
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(),
}
}
}
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,
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("ButtonPage");
t.kw_pair(
"active_page",
inttostr!(self.scrollbar.inner().active_page as u8),
);
for i in 0..self.page_count {
self.paint_dot(i == self.active_page, dot);
dot.y += interval
}
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();
}
}

@ -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();
}
}

@ -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();
}
}

@ -14,6 +14,8 @@ use crate::{
},
};
use super::ButtonStyleSheet;
pub enum ResultPopupMsg {
Confirmed,
}
@ -47,10 +49,11 @@ impl<S: ParagraphStrType> ResultPopup<S> {
.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),
))
});

@ -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()
}
}
}

@ -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();
}
}

@ -1,31 +1,48 @@
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,
util::upy_disable_animation,
},
},
};
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 {
@ -37,6 +54,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,
@ -57,28 +124,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())
};
@ -92,9 +184,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),
@ -107,6 +202,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(),
@ -138,6 +628,44 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// """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,
@ -146,103 +674,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 > > >"#
// )
// }
}

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

Loading…
Cancel
Save