WIP - fixes after rebase
67
ci/build.yml
@ -149,6 +149,39 @@ core fw btconly t1 build:
|
||||
- trezor-fw-btconly-t1-*.*.*-$CI_COMMIT_SHORT_SHA.bin
|
||||
expire_in: 1 week
|
||||
|
||||
core fw R btconly debug build:
|
||||
stage: build
|
||||
<<: *gitlab_caching
|
||||
needs: []
|
||||
variables:
|
||||
TREZOR_MODEL: "R"
|
||||
BITCOIN_ONLY: "1"
|
||||
PYOPT: "0"
|
||||
script:
|
||||
- nix-shell --run "poetry run make -C core build_firmware"
|
||||
- cp core/build/firmware/firmware.bin trezor-fw-btconly-debug-tr-$CORE_VERSION-$CI_COMMIT_SHORT_SHA.bin
|
||||
artifacts:
|
||||
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
|
||||
paths:
|
||||
- trezor-fw-btconly-*.*.*-$CI_COMMIT_SHORT_SHA.bin
|
||||
expire_in: 1 week
|
||||
|
||||
core fw R btconly build:
|
||||
stage: build
|
||||
<<: *gitlab_caching
|
||||
needs: []
|
||||
variables:
|
||||
TREZOR_MODEL: "R"
|
||||
BITCOIN_ONLY: "1"
|
||||
script:
|
||||
- nix-shell --run "poetry run make -C core build_firmware"
|
||||
- cp core/build/firmware/firmware.bin trezor-fw-btconly-tr-$CORE_VERSION-$CI_COMMIT_SHORT_SHA.bin
|
||||
artifacts:
|
||||
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
|
||||
paths:
|
||||
- trezor-fw-btconly-*.*.*-$CI_COMMIT_SHORT_SHA.bin
|
||||
expire_in: 1 week
|
||||
|
||||
# Non-frozen emulator build. This means you still need Python files
|
||||
# present which get interpreted.
|
||||
core unix regular build:
|
||||
@ -278,6 +311,40 @@ core unix frozen R debug build:
|
||||
artifacts:
|
||||
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
|
||||
untracked: true
|
||||
expire_in: 10 weeks
|
||||
|
||||
core unix frozen R debug build arm:
|
||||
image: nixos/nix
|
||||
stage: build
|
||||
<<: *gitlab_caching
|
||||
needs: []
|
||||
variables:
|
||||
PYOPT: "0"
|
||||
TREZOR_MODEL: "R"
|
||||
script:
|
||||
- nix-shell --run "poetry run make -C core build_unix_frozen"
|
||||
- mv core/build/unix/trezor-emu-core core/build/unix/trezor-emu-core-arm
|
||||
artifacts:
|
||||
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
|
||||
untracked: true
|
||||
expire_in: 10 weeks
|
||||
tags:
|
||||
- docker_darwin_arm
|
||||
|
||||
# Debugger build for gdb/lldb.
|
||||
core unix R debugger build:
|
||||
stage: build
|
||||
<<: *gitlab_caching
|
||||
needs: []
|
||||
variables:
|
||||
TREZOR_EMULATOR_FROZEN: "1"
|
||||
TREZOR_MODEL: "R"
|
||||
script:
|
||||
- nix-shell --run "poetry run make -C core build_unix_debug"
|
||||
artifacts:
|
||||
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
|
||||
paths:
|
||||
- core/build/unix
|
||||
expire_in: 1 week
|
||||
|
||||
core unix frozen debug asan build:
|
||||
|
@ -265,6 +265,25 @@ ui tests ui2 fixtures deploy:
|
||||
tags:
|
||||
- deploy
|
||||
|
||||
ui tests R fixtures deploy:
|
||||
stage: deploy
|
||||
variables:
|
||||
DEPLOY_PATH: "${DEPLOY_BASE_DIR}/ui_tests/"
|
||||
BUCKET: "data.trezor.io"
|
||||
GIT_SUBMODULE_STRATEGY: "none"
|
||||
before_script: [] # no poetry
|
||||
needs:
|
||||
- core device R test
|
||||
script:
|
||||
- echo "Deploying to $DEPLOY_PATH"
|
||||
- rsync --delete -va ci/ui_test_records/* "$DEPLOY_PATH"
|
||||
- source ${AWS_DEPLOY_DATA}
|
||||
- aws s3 sync $DEPLOY_PATH s3://$BUCKET/dev/firmware/ui_tests
|
||||
# This "hack" is needed because aws does not have an easy option to generate autoindex. We fetch the one autogenerated by nginx on local server.
|
||||
- wget https://firmware.corp.sldev.cz/ui_tests/ -O index.html && aws s3 cp index.html s3://$BUCKET/dev/firmware/ui_tests/
|
||||
tags:
|
||||
- deploy
|
||||
|
||||
# sync to aws
|
||||
|
||||
sync emulators to aws:
|
||||
|
@ -11,14 +11,14 @@ from tests.ui_tests import read_fixtures # isort:skip
|
||||
read_fixtures()
|
||||
from tests.ui_tests import _hash_files, FILE_HASHES, SCREENS_DIR # isort:skip
|
||||
|
||||
# As in CI we are running T1 and TT tests separately, there will
|
||||
# always be the other model missing.
|
||||
# As in CI we are running tests for more models separately,
|
||||
# there will always be the other models missing.
|
||||
# Therefore, choosing just the cases for our model.
|
||||
if len(sys.argv) > 1 and sys.argv[1].upper() == "T1":
|
||||
model = "T1"
|
||||
if len(sys.argv) > 1:
|
||||
model = sys.argv[1].upper()
|
||||
else:
|
||||
model = "TT"
|
||||
if os.getenv("UI2") == "1":
|
||||
if model == "TT" and os.getenv("UI2") == "1":
|
||||
model += "ui2"
|
||||
model_file_hashes = {k: v for k, v in FILE_HASHES.items() if k.startswith(f"{model}_")}
|
||||
|
||||
|
@ -152,7 +152,7 @@ core device R test:
|
||||
- tests/trezor.log
|
||||
- master_diff
|
||||
when: always
|
||||
expire_in: 1 week
|
||||
expire_in: 4 weeks
|
||||
reports:
|
||||
junit: tests/junit.xml
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -306,6 +306,7 @@ message ResetDevice {
|
||||
optional bool skip_backup = 8; // postpone seed backup to BackupDevice workflow
|
||||
optional bool no_backup = 9; // indicate that no backup is going to be made
|
||||
optional BackupType backup_type = 10 [default=Bip39]; // type of the mnemonic backup
|
||||
optional bool show_tutorial = 11 [default=true]; // whether to show device tutorial
|
||||
}
|
||||
|
||||
/**
|
||||
@ -348,6 +349,7 @@ message RecoveryDevice {
|
||||
optional RecoveryDeviceType type = 8; // supported recovery type
|
||||
optional uint32 u2f_counter = 9; // U2F counter
|
||||
optional bool dry_run = 10; // perform dry-run recovery workflow (for safe mnemonic validation)
|
||||
optional bool show_tutorial = 11 [default=true]; // whether to show device tutorial
|
||||
/**
|
||||
* Type of recovery procedure. These should be used as bitmask, e.g.,
|
||||
* `RecoveryDeviceType_ScrambledWords | RecoveryDeviceType_Matrix`
|
||||
|
1
core/.gitignore
vendored
@ -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
|
||||
UI2 ?= 0
|
||||
|
||||
@ -84,7 +85,7 @@ test: ## run unit tests
|
||||
cd tests ; ./run_tests.sh $(TESTOPTS)
|
||||
|
||||
test_rust: ## run rs unit tests
|
||||
cd embed/rust ; cargo test --target=$(RUST_TARGET) --no-default-features --features model_t$(shell echo $(TREZOR_MODEL) | tr "TR" "tr"),test -- --test-threads=1
|
||||
cd embed/rust ; cargo test $(TESTOPTS) --target=$(RUST_TARGET) --no-default-features --features model_t$(shell echo $(TREZOR_MODEL) | tr "TR" "tr"),test -- --test-threads=1 --nocapture
|
||||
|
||||
test_emu: ## run selected device tests from python-trezor
|
||||
$(EMU_TEST) $(PYTEST) $(TESTPATH)/device_tests $(TESTOPTS)
|
||||
@ -174,7 +175,7 @@ build_unix_frozen: templates build_cross ## build unix port with frozen modules
|
||||
$(SCONS) CFLAGS="$(CFLAGS)" $(UNIX_BUILD_DIR)/trezor-emu-core $(UNIX_PORT_OPTS) TREZOR_MODEL="$(TREZOR_MODEL)" PYOPT="$(PYOPT)" BITCOIN_ONLY="$(BITCOIN_ONLY)" TREZOR_EMULATOR_ASAN="$(ADDRESS_SANITIZER)" UI2="$(UI2)" TREZOR_MEMPERF="$(TREZOR_MEMPERF)" TREZOR_EMULATOR_FROZEN=1
|
||||
|
||||
build_unix_debug: templates ## build unix port
|
||||
$(SCONS) --max-drift=1 CFLAGS="$(CFLAGS)" $(UNIX_BUILD_DIR)/trezor-emu-core $(UNIX_PORT_OPTS) TREZOR_MODEL="$(TREZOR_MODEL)" BITCOIN_ONLY="$(BITCOIN_ONLY)" TREZOR_EMULATOR_ASAN=1 UI2="$(UI2)" TREZOR_EMULATOR_DEBUGGABLE=1
|
||||
$(SCONS) --max-drift=1 CFLAGS="$(CFLAGS)" $(UNIX_BUILD_DIR)/trezor-emu-core $(UNIX_PORT_OPTS) TREZOR_MODEL="$(TREZOR_MODEL)" PYOPT="0" BITCOIN_ONLY="$(BITCOIN_ONLY)" TREZOR_EMULATOR_ASAN=1 UI2="$(UI2)" TREZOR_EMULATOR_DEBUGGABLE=1 TREZOR_EMULATOR_FROZEN="$(TREZOR_EMULATOR_FROZEN)"
|
||||
|
||||
build_cross: ## build mpy-cross port
|
||||
$(MAKE) -C vendor/micropython/mpy-cross $(CROSS_PORT_OPTS)
|
||||
|
@ -613,8 +613,14 @@ if FROZEN:
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/t1.py'))
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/t1.py'))
|
||||
elif TREZOR_MODEL in ('R',):
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/tr/*.py'))
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/tr.py'))
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tr/__init__.py'))
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tr/reset.py'))
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tr/recovery.py'))
|
||||
if EVERYTHING:
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tr/webauthn.py'))
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tr/altcoin.py'))
|
||||
else:
|
||||
raise ValueError('Unknown Trezor model')
|
||||
|
||||
@ -643,7 +649,20 @@ if FROZEN:
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/common/*.py'))
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/common/*/*.py'))
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/debug/*.py'))
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/homescreen/*.py'))
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/homescreen/*.py',
|
||||
# Each homescreen gets included individually below, based on model
|
||||
exclude=[
|
||||
SOURCE_PY_DIR + 'apps/homescreen/tt.py',
|
||||
SOURCE_PY_DIR + 'apps/homescreen/tr.py',
|
||||
SOURCE_PY_DIR + 'apps/homescreen/t1.py',
|
||||
]
|
||||
))
|
||||
if TREZOR_MODEL in ('T',):
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/homescreen/tt.py'))
|
||||
elif TREZOR_MODEL in ('R',):
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/homescreen/tr.py'))
|
||||
elif TREZOR_MODEL in ('1',):
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/homescreen/t1.py'))
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/management/*.py',
|
||||
exclude=[
|
||||
SOURCE_PY_DIR + 'apps/management/sd_protect.py',
|
||||
|
@ -567,6 +567,7 @@ if FROZEN:
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/t1.py'))
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/t1.py'))
|
||||
elif TREZOR_MODEL in ('R',):
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/tr/*.py'))
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/tr.py'))
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tr/__init__.py'))
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tr/reset.py'))
|
||||
@ -602,7 +603,20 @@ if FROZEN:
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/common/*.py'))
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/common/*/*.py'))
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/debug/*.py'))
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/homescreen/*.py'))
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/homescreen/*.py',
|
||||
# Each homescreen gets included individually below, based on model
|
||||
exclude=[
|
||||
SOURCE_PY_DIR + 'apps/homescreen/tt.py',
|
||||
SOURCE_PY_DIR + 'apps/homescreen/tr.py',
|
||||
SOURCE_PY_DIR + 'apps/homescreen/t1.py',
|
||||
]
|
||||
))
|
||||
if TREZOR_MODEL in ('T',):
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/homescreen/tt.py'))
|
||||
elif TREZOR_MODEL in ('R',):
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/homescreen/tr.py'))
|
||||
elif TREZOR_MODEL in ('1',):
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/homescreen/t1.py'))
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/management/*.py',
|
||||
exclude=[
|
||||
SOURCE_PY_DIR + 'apps/management/sd_protect.py',
|
||||
|
BIN
core/assets/check_model_r.png
Normal file
After Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 11 KiB |
BIN
core/assets/model_r/amount.png
Normal file
After Width: | Height: | Size: 180 B |
BIN
core/assets/model_r/bin.png
Normal file
After Width: | Height: | Size: 175 B |
BIN
core/assets/model_r/cancel_for_outline.png
Normal file
After Width: | Height: | Size: 164 B |
BIN
core/assets/model_r/cancel_no_outline.png
Normal file
After Width: | Height: | Size: 125 B |
BIN
core/assets/model_r/down_arrow.png
Normal file
After Width: | Height: | Size: 162 B |
BIN
core/assets/model_r/homescreen.png
Normal file
After Width: | Height: | Size: 342 B |
BIN
core/assets/model_r/left_arm.png
Normal file
After Width: | Height: | Size: 154 B |
BIN
core/assets/model_r/left_arrow.png
Normal file
After Width: | Height: | Size: 157 B |
BIN
core/assets/model_r/lock.png
Normal file
After Width: | Height: | Size: 170 B |
BIN
core/assets/model_r/param.png
Normal file
After Width: | Height: | Size: 159 B |
BIN
core/assets/model_r/right_arm.png
Normal file
After Width: | Height: | Size: 165 B |
BIN
core/assets/model_r/right_arrow.png
Normal file
After Width: | Height: | Size: 156 B |
BIN
core/assets/model_r/up_arrow.png
Normal file
After Width: | Height: | Size: 166 B |
BIN
core/assets/model_r/user.png
Normal file
After Width: | Height: | Size: 176 B |
BIN
core/assets/model_r/wallet.png
Normal file
After Width: | Height: | Size: 178 B |
BIN
core/assets/model_r/warning_left.png
Normal file
After Width: | Height: | Size: 178 B |
BIN
core/assets/model_r/warning_right.png
Normal file
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)},
|
||||
|
203
core/embed/extmod/modtrezorui/font_unifont_bold_16.c
Normal file
@ -0,0 +1,203 @@
|
||||
#include <stdint.h>
|
||||
|
||||
// clang-format off
|
||||
|
||||
// - the first two bytes are width and height of the glyph
|
||||
// - the third, fourth and fifth bytes are advance, bearingX and bearingY of the horizontal metrics of the glyph
|
||||
// - the rest is packed 1-bit glyph data
|
||||
|
||||
/* */ static const uint8_t Font_Unifont_Bold_16_glyph_32[] = { 0, 0, 7, 0, 0 }; // hand-changed to 7 to have 9px space between words
|
||||
/* ! */ static const uint8_t Font_Unifont_Bold_16_glyph_33[] = { 2, 10, 7, 2, 10, 255, 252, 240 };
|
||||
/* " */ static const uint8_t Font_Unifont_Bold_16_glyph_34[] = { 6, 4, 7, 0, 12, 207, 60, 209, 0 };
|
||||
/* # */ static const uint8_t Font_Unifont_Bold_16_glyph_35[] = { 7, 10, 8, 0, 10, 54, 108, 223, 246, 205, 191, 236, 217, 176 };
|
||||
/* $ */ static const uint8_t Font_Unifont_Bold_16_glyph_36[] = { 7, 10, 8, 0, 10, 16, 251, 94, 183, 3, 181, 235, 124, 32 };
|
||||
/* % */ static const uint8_t Font_Unifont_Bold_16_glyph_37[] = { 7, 10, 8, 0, 10, 97, 102, 211, 65, 2, 11, 43, 150, 24 };
|
||||
/* & */ static const uint8_t Font_Unifont_Bold_16_glyph_38[] = { 8, 10, 8, 0, 10, 56, 108, 108, 104, 48, 107, 206, 204, 206, 122, 0 };
|
||||
/* ' */ static const uint8_t Font_Unifont_Bold_16_glyph_39[] = { 2, 4, 7, 2, 12, 253, 0 };
|
||||
/* ( */ static const uint8_t Font_Unifont_Bold_16_glyph_40[] = { 4, 12, 7, 2, 11, 54, 108, 204, 204, 198, 99, 0 };
|
||||
/* ) */ static const uint8_t Font_Unifont_Bold_16_glyph_41[] = { 4, 12, 7, 0, 11, 198, 99, 51, 51, 54, 108, 0 };
|
||||
/* * */ static const uint8_t Font_Unifont_Bold_16_glyph_42[] = { 7, 7, 8, 0, 8, 17, 37, 81, 197, 82, 68, 0 };
|
||||
/* + */ static const uint8_t Font_Unifont_Bold_16_glyph_43[] = { 7, 7, 8, 0, 8, 16, 32, 71, 241, 2, 4, 0 };
|
||||
/* , */ static const uint8_t Font_Unifont_Bold_16_glyph_44[] = { 3, 4, 7, 1, 2, 237, 224 };
|
||||
/* - */ static const uint8_t Font_Unifont_Bold_16_glyph_45[] = { 5, 1, 7, 0, 5, 248 };
|
||||
/* . */ static const uint8_t Font_Unifont_Bold_16_glyph_46[] = { 2, 2, 7, 2, 2, 240 };
|
||||
/* / */ static const uint8_t Font_Unifont_Bold_16_glyph_47[] = { 6, 10, 7, 0, 10, 12, 49, 132, 48, 194, 24, 195, 0 };
|
||||
/* 0 */ static const uint8_t Font_Unifont_Bold_16_glyph_48[] = { 7, 10, 8, 0, 10, 56, 219, 30, 125, 122, 249, 227, 108, 112 };
|
||||
/* 1 */ static const uint8_t Font_Unifont_Bold_16_glyph_49[] = { 6, 10, 7, 0, 10, 51, 195, 12, 48, 195, 12, 51, 240 };
|
||||
/* 2 */ static const uint8_t Font_Unifont_Bold_16_glyph_50[] = { 7, 10, 8, 0, 10, 125, 143, 24, 48, 195, 12, 112, 193, 252 };
|
||||
/* 3 */ static const uint8_t Font_Unifont_Bold_16_glyph_51[] = { 7, 10, 8, 0, 10, 125, 143, 24, 49, 192, 193, 227, 198, 248 };
|
||||
/* 4 */ static const uint8_t Font_Unifont_Bold_16_glyph_52[] = { 7, 10, 8, 0, 10, 12, 120, 179, 100, 217, 191, 134, 12, 24 };
|
||||
/* 5 */ static const uint8_t Font_Unifont_Bold_16_glyph_53[] = { 7, 10, 8, 0, 10, 255, 131, 6, 15, 193, 193, 227, 198, 248 };
|
||||
/* 6 */ static const uint8_t Font_Unifont_Bold_16_glyph_54[] = { 7, 10, 8, 0, 10, 60, 195, 6, 15, 216, 241, 227, 198, 248 };
|
||||
/* 7 */ static const uint8_t Font_Unifont_Bold_16_glyph_55[] = { 6, 10, 7, 0, 10, 252, 48, 195, 24, 99, 12, 97, 128 };
|
||||
/* 8 */ static const uint8_t Font_Unifont_Bold_16_glyph_56[] = { 7, 10, 8, 0, 10, 125, 143, 30, 55, 216, 241, 227, 198, 248 };
|
||||
/* 9 */ static const uint8_t Font_Unifont_Bold_16_glyph_57[] = { 7, 10, 8, 0, 10, 125, 143, 30, 60, 111, 193, 131, 12, 240 };
|
||||
/* : */ static const uint8_t Font_Unifont_Bold_16_glyph_58[] = { 2, 7, 7, 2, 8, 240, 60 };
|
||||
/* ; */ static const uint8_t Font_Unifont_Bold_16_glyph_59[] = { 2, 9, 7, 2, 8, 240, 61, 128 };
|
||||
/* < */ static const uint8_t Font_Unifont_Bold_16_glyph_60[] = { 6, 9, 7, 0, 9, 12, 99, 24, 193, 131, 6, 12 };
|
||||
/* = */ static const uint8_t Font_Unifont_Bold_16_glyph_61[] = { 6, 5, 7, 0, 7, 252, 0, 0, 252 };
|
||||
/* > */ static const uint8_t Font_Unifont_Bold_16_glyph_62[] = { 6, 9, 7, 0, 9, 193, 131, 6, 12, 99, 24, 192 };
|
||||
/* ? */ static const uint8_t Font_Unifont_Bold_16_glyph_63[] = { 7, 10, 8, 0, 10, 125, 143, 24, 48, 195, 6, 0, 24, 48 };
|
||||
/* @ */ static const uint8_t Font_Unifont_Bold_16_glyph_64[] = { 7, 10, 8, 0, 10, 60, 134, 109, 187, 118, 237, 205, 64, 124 };
|
||||
/* A */ static const uint8_t Font_Unifont_Bold_16_glyph_65[] = { 7, 10, 8, 0, 10, 56, 249, 182, 60, 120, 255, 227, 199, 140 };
|
||||
/* B */ static const uint8_t Font_Unifont_Bold_16_glyph_66[] = { 7, 10, 8, 0, 10, 253, 143, 30, 63, 216, 241, 227, 199, 248 };
|
||||
/* C */ static const uint8_t Font_Unifont_Bold_16_glyph_67[] = { 7, 10, 8, 0, 10, 125, 143, 30, 12, 24, 48, 99, 198, 248 };
|
||||
/* D */ static const uint8_t Font_Unifont_Bold_16_glyph_68[] = { 7, 10, 8, 0, 10, 241, 155, 30, 60, 120, 241, 227, 205, 224 };
|
||||
/* E */ static const uint8_t Font_Unifont_Bold_16_glyph_69[] = { 6, 10, 7, 0, 10, 255, 12, 48, 251, 12, 48, 195, 240 };
|
||||
/* F */ static const uint8_t Font_Unifont_Bold_16_glyph_70[] = { 6, 10, 7, 0, 10, 255, 12, 48, 195, 236, 48, 195, 0 };
|
||||
/* G */ static const uint8_t Font_Unifont_Bold_16_glyph_71[] = { 7, 10, 8, 0, 10, 125, 143, 30, 12, 27, 241, 227, 206, 236 };
|
||||
/* H */ static const uint8_t Font_Unifont_Bold_16_glyph_72[] = { 7, 10, 8, 0, 10, 199, 143, 30, 63, 248, 241, 227, 199, 140 };
|
||||
/* I */ static const uint8_t Font_Unifont_Bold_16_glyph_73[] = { 6, 10, 7, 0, 10, 252, 195, 12, 48, 195, 12, 51, 240 };
|
||||
/* J */ static const uint8_t Font_Unifont_Bold_16_glyph_74[] = { 7, 10, 8, 0, 10, 62, 24, 48, 96, 193, 131, 102, 204, 240 };
|
||||
/* K */ static const uint8_t Font_Unifont_Bold_16_glyph_75[] = { 7, 10, 8, 0, 10, 199, 143, 54, 207, 28, 62, 110, 207, 140 };
|
||||
/* L */ static const uint8_t Font_Unifont_Bold_16_glyph_76[] = { 6, 10, 7, 0, 10, 195, 12, 48, 195, 12, 48, 195, 240 };
|
||||
/* M */ static const uint8_t Font_Unifont_Bold_16_glyph_77[] = { 7, 10, 8, 0, 10, 131, 143, 31, 127, 250, 245, 227, 199, 140 };
|
||||
/* N */ static const uint8_t Font_Unifont_Bold_16_glyph_78[] = { 7, 10, 8, 0, 10, 199, 207, 158, 189, 122, 245, 231, 207, 140 };
|
||||
/* O */ static const uint8_t Font_Unifont_Bold_16_glyph_79[] = { 7, 10, 8, 0, 10, 125, 143, 30, 60, 120, 241, 227, 198, 248 };
|
||||
/* P */ static const uint8_t Font_Unifont_Bold_16_glyph_80[] = { 7, 10, 8, 0, 10, 253, 143, 30, 60, 127, 176, 96, 193, 128 };
|
||||
/* Q */ static const uint8_t Font_Unifont_Bold_16_glyph_81[] = { 7, 11, 8, 0, 10, 125, 143, 30, 60, 120, 241, 235, 238, 112, 24 };
|
||||
/* R */ static const uint8_t Font_Unifont_Bold_16_glyph_82[] = { 7, 10, 8, 0, 10, 253, 143, 30, 60, 127, 182, 102, 199, 140 };
|
||||
/* S */ static const uint8_t Font_Unifont_Bold_16_glyph_83[] = { 7, 10, 8, 0, 10, 125, 143, 31, 7, 135, 131, 227, 198, 248 };
|
||||
/* T */ static const uint8_t Font_Unifont_Bold_16_glyph_84[] = { 7, 10, 8, 0, 10, 254, 48, 96, 193, 131, 6, 12, 24, 48 };
|
||||
/* U */ static const uint8_t Font_Unifont_Bold_16_glyph_85[] = { 7, 10, 8, 0, 10, 199, 143, 30, 60, 120, 241, 227, 238, 248 };
|
||||
/* V */ static const uint8_t Font_Unifont_Bold_16_glyph_86[] = { 7, 10, 8, 0, 10, 199, 143, 26, 38, 205, 155, 20, 40, 112 };
|
||||
/* W */ static const uint8_t Font_Unifont_Bold_16_glyph_87[] = { 7, 10, 8, 0, 10, 199, 143, 30, 189, 122, 245, 255, 238, 136 };
|
||||
/* X */ static const uint8_t Font_Unifont_Bold_16_glyph_88[] = { 7, 10, 8, 0, 10, 199, 141, 179, 99, 135, 27, 54, 199, 140 };
|
||||
/* Y */ static const uint8_t Font_Unifont_Bold_16_glyph_89[] = { 6, 10, 7, 0, 10, 207, 60, 243, 73, 227, 12, 48, 192 };
|
||||
/* Z */ static const uint8_t Font_Unifont_Bold_16_glyph_90[] = { 7, 10, 8, 0, 10, 254, 12, 56, 225, 135, 28, 112, 193, 252 };
|
||||
/* [ */ static const uint8_t Font_Unifont_Bold_16_glyph_91[] = { 4, 12, 7, 2, 11, 252, 204, 204, 204, 204, 207, 0 };
|
||||
/* \ */ static const uint8_t Font_Unifont_Bold_16_glyph_92[] = { 6, 10, 7, 0, 10, 195, 6, 8, 48, 193, 6, 12, 48 };
|
||||
/* ] */ static const uint8_t Font_Unifont_Bold_16_glyph_93[] = { 4, 12, 7, 0, 11, 243, 51, 51, 51, 51, 63, 0 };
|
||||
/* ^ */ static const uint8_t Font_Unifont_Bold_16_glyph_94[] = { 7, 3, 8, 0, 12, 56, 219, 24 };
|
||||
/* _ */ static const uint8_t Font_Unifont_Bold_16_glyph_95[] = { 7, 1, 7, 0, 0, 254 };
|
||||
/* ` */ static const uint8_t Font_Unifont_Bold_16_glyph_96[] = { 4, 3, 7, 0, 13, 198, 48 };
|
||||
/* a */ static const uint8_t Font_Unifont_Bold_16_glyph_97[] = { 7, 8, 8, 0, 8, 125, 140, 27, 252, 120, 243, 187, 0 };
|
||||
/* b */ static const uint8_t Font_Unifont_Bold_16_glyph_98[] = { 7, 11, 8, 0, 11, 193, 131, 6, 238, 120, 241, 227, 199, 207, 112 };
|
||||
/* c */ static const uint8_t Font_Unifont_Bold_16_glyph_99[] = { 7, 8, 8, 0, 8, 125, 143, 30, 12, 24, 241, 190, 0 };
|
||||
/* d */ static const uint8_t Font_Unifont_Bold_16_glyph_100[] = { 7, 11, 8, 0, 11, 6, 12, 25, 188, 248, 241, 227, 199, 156, 216 };
|
||||
/* e */ static const uint8_t Font_Unifont_Bold_16_glyph_101[] = { 7, 8, 8, 0, 8, 125, 143, 31, 252, 24, 241, 190, 0 };
|
||||
/* f */ static const uint8_t Font_Unifont_Bold_16_glyph_102[] = { 7, 11, 8, 0, 11, 30, 96, 193, 143, 230, 12, 24, 48, 97, 240 };
|
||||
/* g */ static const uint8_t Font_Unifont_Bold_16_glyph_103[] = { 7, 11, 8, 0, 9, 2, 247, 54, 108, 207, 8, 62, 207, 141, 240 };
|
||||
/* h */ static const uint8_t Font_Unifont_Bold_16_glyph_104[] = { 7, 11, 8, 0, 11, 193, 131, 6, 238, 120, 241, 227, 199, 143, 24 };
|
||||
/* i */ static const uint8_t Font_Unifont_Bold_16_glyph_105[] = { 6, 11, 7, 0, 11, 48, 192, 60, 48, 195, 12, 48, 207, 192 };
|
||||
/* j */ static const uint8_t Font_Unifont_Bold_16_glyph_106[] = { 6, 13, 8, 0, 11, 24, 96, 31, 12, 48, 195, 12, 60, 246, 112 };
|
||||
/* k */ static const uint8_t Font_Unifont_Bold_16_glyph_107[] = { 7, 11, 8, 0, 11, 193, 131, 6, 60, 251, 60, 120, 217, 159, 24 };
|
||||
/* l */ static const uint8_t Font_Unifont_Bold_16_glyph_108[] = { 6, 11, 7, 0, 11, 240, 195, 12, 48, 195, 12, 48, 207, 192 };
|
||||
/* m */ static const uint8_t Font_Unifont_Bold_16_glyph_109[] = { 7, 8, 8, 0, 8, 237, 175, 94, 189, 122, 245, 235, 0 };
|
||||
/* n */ static const uint8_t Font_Unifont_Bold_16_glyph_110[] = { 7, 8, 8, 0, 8, 221, 207, 30, 60, 120, 241, 227, 0 };
|
||||
/* o */ static const uint8_t Font_Unifont_Bold_16_glyph_111[] = { 7, 8, 8, 0, 8, 125, 143, 30, 60, 120, 241, 190, 0 };
|
||||
/* p */ static const uint8_t Font_Unifont_Bold_16_glyph_112[] = { 7, 10, 8, 0, 8, 221, 207, 30, 60, 120, 249, 238, 193, 128 };
|
||||
/* q */ static const uint8_t Font_Unifont_Bold_16_glyph_113[] = { 7, 10, 8, 0, 8, 119, 159, 30, 60, 120, 243, 187, 6, 12 };
|
||||
/* r */ static const uint8_t Font_Unifont_Bold_16_glyph_114[] = { 7, 8, 8, 0, 8, 221, 207, 30, 12, 24, 48, 96, 0 };
|
||||
/* s */ static const uint8_t Font_Unifont_Bold_16_glyph_115[] = { 7, 8, 8, 0, 8, 125, 143, 27, 129, 216, 241, 190, 0 };
|
||||
/* t */ static const uint8_t Font_Unifont_Bold_16_glyph_116[] = { 7, 10, 8, 0, 10, 48, 96, 199, 243, 6, 12, 24, 48, 60 };
|
||||
/* u */ static const uint8_t Font_Unifont_Bold_16_glyph_117[] = { 7, 8, 8, 0, 8, 199, 143, 30, 60, 120, 243, 187, 0 };
|
||||
/* v */ static const uint8_t Font_Unifont_Bold_16_glyph_118[] = { 7, 8, 8, 0, 8, 199, 143, 26, 38, 205, 142, 28, 0 };
|
||||
/* w */ static const uint8_t Font_Unifont_Bold_16_glyph_119[] = { 7, 8, 8, 0, 8, 199, 175, 94, 189, 122, 245, 182, 0 };
|
||||
/* x */ static const uint8_t Font_Unifont_Bold_16_glyph_120[] = { 7, 8, 8, 0, 8, 199, 141, 177, 195, 141, 177, 227, 0 };
|
||||
/* y */ static const uint8_t Font_Unifont_Bold_16_glyph_121[] = { 7, 10, 8, 0, 8, 199, 143, 30, 60, 109, 205, 131, 6, 248 };
|
||||
/* z */ static const uint8_t Font_Unifont_Bold_16_glyph_122[] = { 7, 8, 8, 0, 8, 254, 12, 56, 227, 142, 56, 127, 0 };
|
||||
/* { */ static const uint8_t Font_Unifont_Bold_16_glyph_123[] = { 5, 13, 7, 1, 11, 59, 24, 99, 51, 12, 49, 152, 195, 128 };
|
||||
/* | */ static const uint8_t Font_Unifont_Bold_16_glyph_124[] = { 2, 14, 7, 2, 12, 255, 255, 255, 240 };
|
||||
/* } */ static const uint8_t Font_Unifont_Bold_16_glyph_125[] = { 5, 13, 7, 0, 11, 225, 140, 198, 24, 102, 99, 12, 110, 0 };
|
||||
/* ~ */ static const uint8_t Font_Unifont_Bold_16_glyph_126[] = { 7, 3, 8, 0, 11, 99, 118, 48 };
|
||||
|
||||
const uint8_t Font_Unifont_Bold_16_glyph_nonprintable[] = { 7, 10, 8, 0, 10, 130, 112, 231, 207, 60, 249, 255, 231, 207 };
|
||||
|
||||
const uint8_t * const Font_Unifont_Bold_16[126 + 1 - 32] = {
|
||||
Font_Unifont_Bold_16_glyph_32,
|
||||
Font_Unifont_Bold_16_glyph_33,
|
||||
Font_Unifont_Bold_16_glyph_34,
|
||||
Font_Unifont_Bold_16_glyph_35,
|
||||
Font_Unifont_Bold_16_glyph_36,
|
||||
Font_Unifont_Bold_16_glyph_37,
|
||||
Font_Unifont_Bold_16_glyph_38,
|
||||
Font_Unifont_Bold_16_glyph_39,
|
||||
Font_Unifont_Bold_16_glyph_40,
|
||||
Font_Unifont_Bold_16_glyph_41,
|
||||
Font_Unifont_Bold_16_glyph_42,
|
||||
Font_Unifont_Bold_16_glyph_43,
|
||||
Font_Unifont_Bold_16_glyph_44,
|
||||
Font_Unifont_Bold_16_glyph_45,
|
||||
Font_Unifont_Bold_16_glyph_46,
|
||||
Font_Unifont_Bold_16_glyph_47,
|
||||
Font_Unifont_Bold_16_glyph_48,
|
||||
Font_Unifont_Bold_16_glyph_49,
|
||||
Font_Unifont_Bold_16_glyph_50,
|
||||
Font_Unifont_Bold_16_glyph_51,
|
||||
Font_Unifont_Bold_16_glyph_52,
|
||||
Font_Unifont_Bold_16_glyph_53,
|
||||
Font_Unifont_Bold_16_glyph_54,
|
||||
Font_Unifont_Bold_16_glyph_55,
|
||||
Font_Unifont_Bold_16_glyph_56,
|
||||
Font_Unifont_Bold_16_glyph_57,
|
||||
Font_Unifont_Bold_16_glyph_58,
|
||||
Font_Unifont_Bold_16_glyph_59,
|
||||
Font_Unifont_Bold_16_glyph_60,
|
||||
Font_Unifont_Bold_16_glyph_61,
|
||||
Font_Unifont_Bold_16_glyph_62,
|
||||
Font_Unifont_Bold_16_glyph_63,
|
||||
Font_Unifont_Bold_16_glyph_64,
|
||||
Font_Unifont_Bold_16_glyph_65,
|
||||
Font_Unifont_Bold_16_glyph_66,
|
||||
Font_Unifont_Bold_16_glyph_67,
|
||||
Font_Unifont_Bold_16_glyph_68,
|
||||
Font_Unifont_Bold_16_glyph_69,
|
||||
Font_Unifont_Bold_16_glyph_70,
|
||||
Font_Unifont_Bold_16_glyph_71,
|
||||
Font_Unifont_Bold_16_glyph_72,
|
||||
Font_Unifont_Bold_16_glyph_73,
|
||||
Font_Unifont_Bold_16_glyph_74,
|
||||
Font_Unifont_Bold_16_glyph_75,
|
||||
Font_Unifont_Bold_16_glyph_76,
|
||||
Font_Unifont_Bold_16_glyph_77,
|
||||
Font_Unifont_Bold_16_glyph_78,
|
||||
Font_Unifont_Bold_16_glyph_79,
|
||||
Font_Unifont_Bold_16_glyph_80,
|
||||
Font_Unifont_Bold_16_glyph_81,
|
||||
Font_Unifont_Bold_16_glyph_82,
|
||||
Font_Unifont_Bold_16_glyph_83,
|
||||
Font_Unifont_Bold_16_glyph_84,
|
||||
Font_Unifont_Bold_16_glyph_85,
|
||||
Font_Unifont_Bold_16_glyph_86,
|
||||
Font_Unifont_Bold_16_glyph_87,
|
||||
Font_Unifont_Bold_16_glyph_88,
|
||||
Font_Unifont_Bold_16_glyph_89,
|
||||
Font_Unifont_Bold_16_glyph_90,
|
||||
Font_Unifont_Bold_16_glyph_91,
|
||||
Font_Unifont_Bold_16_glyph_92,
|
||||
Font_Unifont_Bold_16_glyph_93,
|
||||
Font_Unifont_Bold_16_glyph_94,
|
||||
Font_Unifont_Bold_16_glyph_95,
|
||||
Font_Unifont_Bold_16_glyph_96,
|
||||
Font_Unifont_Bold_16_glyph_97,
|
||||
Font_Unifont_Bold_16_glyph_98,
|
||||
Font_Unifont_Bold_16_glyph_99,
|
||||
Font_Unifont_Bold_16_glyph_100,
|
||||
Font_Unifont_Bold_16_glyph_101,
|
||||
Font_Unifont_Bold_16_glyph_102,
|
||||
Font_Unifont_Bold_16_glyph_103,
|
||||
Font_Unifont_Bold_16_glyph_104,
|
||||
Font_Unifont_Bold_16_glyph_105,
|
||||
Font_Unifont_Bold_16_glyph_106,
|
||||
Font_Unifont_Bold_16_glyph_107,
|
||||
Font_Unifont_Bold_16_glyph_108,
|
||||
Font_Unifont_Bold_16_glyph_109,
|
||||
Font_Unifont_Bold_16_glyph_110,
|
||||
Font_Unifont_Bold_16_glyph_111,
|
||||
Font_Unifont_Bold_16_glyph_112,
|
||||
Font_Unifont_Bold_16_glyph_113,
|
||||
Font_Unifont_Bold_16_glyph_114,
|
||||
Font_Unifont_Bold_16_glyph_115,
|
||||
Font_Unifont_Bold_16_glyph_116,
|
||||
Font_Unifont_Bold_16_glyph_117,
|
||||
Font_Unifont_Bold_16_glyph_118,
|
||||
Font_Unifont_Bold_16_glyph_119,
|
||||
Font_Unifont_Bold_16_glyph_120,
|
||||
Font_Unifont_Bold_16_glyph_121,
|
||||
Font_Unifont_Bold_16_glyph_122,
|
||||
Font_Unifont_Bold_16_glyph_123,
|
||||
Font_Unifont_Bold_16_glyph_124,
|
||||
Font_Unifont_Bold_16_glyph_125,
|
||||
Font_Unifont_Bold_16_glyph_126,
|
||||
};
|
7
core/embed/extmod/modtrezorui/font_unifont_bold_16.h
Normal file
@ -0,0 +1,7 @@
|
||||
#include <stdint.h>
|
||||
|
||||
#if TREZOR_FONT_BPP != 1
|
||||
#error Wrong TREZOR_FONT_BPP (expected 1)
|
||||
#endif
|
||||
extern const uint8_t* const Font_Unifont_Bold_16[126 + 1 - 32];
|
||||
extern const uint8_t Font_Unifont_Bold_16_glyph_nonprintable[];
|
203
core/embed/extmod/modtrezorui/font_unifont_regular_16.c
Normal file
@ -0,0 +1,203 @@
|
||||
#include <stdint.h>
|
||||
|
||||
// clang-format off
|
||||
|
||||
// - the first two bytes are width and height of the glyph
|
||||
// - the third, fourth and fifth bytes are advance, bearingX and bearingY of the horizontal metrics of the glyph
|
||||
// - the rest is packed 1-bit glyph data
|
||||
|
||||
/* */ static const uint8_t Font_Unifont_Regular_16_glyph_32[] = { 0, 0, 8, 0, 0 };
|
||||
/* ! */ static const uint8_t Font_Unifont_Regular_16_glyph_33[] = { 1, 10, 7, 3, 10, 254, 192 };
|
||||
/* " */ static const uint8_t Font_Unifont_Regular_16_glyph_34[] = { 5, 4, 7, 1, 12, 140, 99, 16 };
|
||||
/* # */ static const uint8_t Font_Unifont_Regular_16_glyph_35[] = { 6, 10, 7, 0, 10, 36, 146, 127, 73, 47, 228, 146, 64 };
|
||||
/* $ */ static const uint8_t Font_Unifont_Regular_16_glyph_36[] = { 7, 10, 7, 0, 10, 16, 250, 76, 135, 3, 132, 201, 124, 32 };
|
||||
/* % */ static const uint8_t Font_Unifont_Regular_16_glyph_37[] = { 7, 10, 7, 0, 10, 99, 42, 83, 65, 2, 11, 41, 83, 24 };
|
||||
/* & */ static const uint8_t Font_Unifont_Regular_16_glyph_38[] = { 7, 10, 7, 0, 10, 56, 137, 17, 67, 10, 98, 194, 140, 228 };
|
||||
/* ' */ static const uint8_t Font_Unifont_Regular_16_glyph_39[] = { 1, 4, 7, 3, 12, 240 };
|
||||
/* ( */ static const uint8_t Font_Unifont_Regular_16_glyph_40[] = { 3, 12, 7, 2, 11, 41, 73, 36, 137, 16 };
|
||||
/* ) */ static const uint8_t Font_Unifont_Regular_16_glyph_41[] = { 3, 12, 7, 1, 11, 137, 18, 73, 41, 64 };
|
||||
/* * */ static const uint8_t Font_Unifont_Regular_16_glyph_42[] = { 7, 7, 7, 0, 8, 17, 37, 81, 197, 82, 68, 0 };
|
||||
/* + */ static const uint8_t Font_Unifont_Regular_16_glyph_43[] = { 7, 7, 7, 0, 8, 16, 32, 71, 241, 2, 4, 0 };
|
||||
/* , */ static const uint8_t Font_Unifont_Regular_16_glyph_44[] = { 2, 4, 7, 2, 2, 214, 0 };
|
||||
/* - */ static const uint8_t Font_Unifont_Regular_16_glyph_45[] = { 4, 1, 7, 1, 5, 240 };
|
||||
/* . */ static const uint8_t Font_Unifont_Regular_16_glyph_46[] = { 2, 2, 7, 2, 2, 240 };
|
||||
/* / */ static const uint8_t Font_Unifont_Regular_16_glyph_47[] = { 6, 10, 7, 0, 10, 4, 16, 132, 16, 130, 16, 130, 0 };
|
||||
/* 0 */ static const uint8_t Font_Unifont_Regular_16_glyph_48[] = { 6, 10, 7, 0, 10, 49, 40, 99, 150, 156, 97, 72, 192 };
|
||||
/* 1 */ static const uint8_t Font_Unifont_Regular_16_glyph_49[] = { 5, 10, 7, 1, 10, 35, 40, 66, 16, 132, 39, 192 };
|
||||
/* 2 */ static const uint8_t Font_Unifont_Regular_16_glyph_50[] = { 6, 10, 7, 0, 10, 122, 24, 65, 24, 132, 32, 131, 240 };
|
||||
/* 3 */ static const uint8_t Font_Unifont_Regular_16_glyph_51[] = { 6, 10, 7, 0, 10, 122, 24, 65, 56, 16, 97, 133, 224 };
|
||||
/* 4 */ static const uint8_t Font_Unifont_Regular_16_glyph_52[] = { 6, 10, 7, 0, 10, 8, 98, 146, 138, 47, 194, 8, 32 };
|
||||
/* 5 */ static const uint8_t Font_Unifont_Regular_16_glyph_53[] = { 6, 10, 7, 0, 10, 254, 8, 32, 248, 16, 65, 133, 224 };
|
||||
/* 6 */ static const uint8_t Font_Unifont_Regular_16_glyph_54[] = { 6, 10, 7, 0, 10, 57, 8, 32, 250, 24, 97, 133, 224 };
|
||||
/* 7 */ static const uint8_t Font_Unifont_Regular_16_glyph_55[] = { 6, 10, 7, 0, 10, 252, 16, 66, 8, 33, 4, 16, 64 };
|
||||
/* 8 */ static const uint8_t Font_Unifont_Regular_16_glyph_56[] = { 6, 10, 7, 0, 10, 122, 24, 97, 122, 24, 97, 133, 224 };
|
||||
/* 9 */ static const uint8_t Font_Unifont_Regular_16_glyph_57[] = { 6, 10, 7, 0, 10, 122, 24, 97, 124, 16, 65, 9, 192 };
|
||||
/* : */ static const uint8_t Font_Unifont_Regular_16_glyph_58[] = { 2, 7, 7, 2, 8, 240, 60 };
|
||||
/* ; */ static const uint8_t Font_Unifont_Regular_16_glyph_59[] = { 2, 9, 7, 2, 8, 240, 53, 128 };
|
||||
/* < */ static const uint8_t Font_Unifont_Regular_16_glyph_60[] = { 5, 9, 7, 1, 9, 8, 136, 136, 32, 130, 8 };
|
||||
/* = */ static const uint8_t Font_Unifont_Regular_16_glyph_61[] = { 6, 5, 7, 0, 7, 252, 0, 0, 252 };
|
||||
/* > */ static const uint8_t Font_Unifont_Regular_16_glyph_62[] = { 5, 9, 7, 0, 9, 130, 8, 32, 136, 136, 128 };
|
||||
/* ? */ static const uint8_t Font_Unifont_Regular_16_glyph_63[] = { 6, 10, 7, 0, 10, 122, 24, 65, 8, 65, 0, 16, 64 };
|
||||
/* @ */ static const uint8_t Font_Unifont_Regular_16_glyph_64[] = { 6, 10, 7, 0, 10, 57, 25, 107, 166, 154, 103, 64, 240 };
|
||||
/* A */ static const uint8_t Font_Unifont_Regular_16_glyph_65[] = { 6, 10, 7, 0, 10, 49, 36, 161, 135, 248, 97, 134, 16 };
|
||||
/* B */ static const uint8_t Font_Unifont_Regular_16_glyph_66[] = { 6, 10, 7, 0, 10, 250, 24, 97, 250, 24, 97, 135, 224 };
|
||||
/* C */ static const uint8_t Font_Unifont_Regular_16_glyph_67[] = { 6, 10, 7, 0, 10, 122, 24, 96, 130, 8, 33, 133, 224 };
|
||||
/* D */ static const uint8_t Font_Unifont_Regular_16_glyph_68[] = { 6, 10, 7, 0, 10, 242, 40, 97, 134, 24, 97, 139, 192 };
|
||||
/* E */ static const uint8_t Font_Unifont_Regular_16_glyph_69[] = { 6, 10, 7, 0, 10, 254, 8, 32, 250, 8, 32, 131, 240 };
|
||||
/* F */ static const uint8_t Font_Unifont_Regular_16_glyph_70[] = { 6, 10, 7, 0, 10, 254, 8, 32, 250, 8, 32, 130, 0 };
|
||||
/* G */ static const uint8_t Font_Unifont_Regular_16_glyph_71[] = { 6, 10, 7, 0, 10, 122, 24, 96, 130, 120, 97, 141, 208 };
|
||||
/* H */ static const uint8_t Font_Unifont_Regular_16_glyph_72[] = { 6, 10, 7, 0, 10, 134, 24, 97, 254, 24, 97, 134, 16 };
|
||||
/* I */ static const uint8_t Font_Unifont_Regular_16_glyph_73[] = { 5, 10, 7, 1, 10, 249, 8, 66, 16, 132, 39, 192 };
|
||||
/* J */ static const uint8_t Font_Unifont_Regular_16_glyph_74[] = { 7, 10, 7, 0, 10, 62, 16, 32, 64, 129, 2, 68, 136, 224 };
|
||||
/* K */ static const uint8_t Font_Unifont_Regular_16_glyph_75[] = { 6, 10, 7, 0, 10, 134, 41, 40, 195, 10, 36, 138, 16 };
|
||||
/* L */ static const uint8_t Font_Unifont_Regular_16_glyph_76[] = { 6, 10, 7, 0, 10, 130, 8, 32, 130, 8, 32, 131, 240 };
|
||||
/* M */ static const uint8_t Font_Unifont_Regular_16_glyph_77[] = { 6, 10, 7, 0, 10, 134, 28, 243, 182, 216, 97, 134, 16 };
|
||||
/* N */ static const uint8_t Font_Unifont_Regular_16_glyph_78[] = { 6, 10, 7, 0, 10, 135, 28, 105, 166, 89, 99, 142, 16 };
|
||||
/* O */ static const uint8_t Font_Unifont_Regular_16_glyph_79[] = { 6, 10, 7, 0, 10, 122, 24, 97, 134, 24, 97, 133, 224 };
|
||||
/* P */ static const uint8_t Font_Unifont_Regular_16_glyph_80[] = { 6, 10, 7, 0, 10, 250, 24, 97, 250, 8, 32, 130, 0 };
|
||||
/* Q */ static const uint8_t Font_Unifont_Regular_16_glyph_81[] = { 7, 11, 7, 0, 10, 121, 10, 20, 40, 80, 161, 90, 204, 240, 24 };
|
||||
/* R */ static const uint8_t Font_Unifont_Regular_16_glyph_82[] = { 6, 10, 7, 0, 10, 250, 24, 97, 250, 72, 162, 134, 16 };
|
||||
/* S */ static const uint8_t Font_Unifont_Regular_16_glyph_83[] = { 6, 10, 7, 0, 10, 122, 24, 96, 96, 96, 97, 133, 224 };
|
||||
/* T */ static const uint8_t Font_Unifont_Regular_16_glyph_84[] = { 7, 10, 7, 0, 10, 254, 32, 64, 129, 2, 4, 8, 16, 32 };
|
||||
/* U */ static const uint8_t Font_Unifont_Regular_16_glyph_85[] = { 6, 10, 7, 0, 10, 134, 24, 97, 134, 24, 97, 133, 224 };
|
||||
/* V */ static const uint8_t Font_Unifont_Regular_16_glyph_86[] = { 7, 10, 7, 0, 10, 131, 6, 10, 36, 72, 138, 20, 16, 32 };
|
||||
/* W */ static const uint8_t Font_Unifont_Regular_16_glyph_87[] = { 6, 10, 7, 0, 10, 134, 24, 97, 182, 220, 243, 134, 16 };
|
||||
/* X */ static const uint8_t Font_Unifont_Regular_16_glyph_88[] = { 6, 10, 7, 0, 10, 134, 20, 146, 48, 196, 146, 134, 16 };
|
||||
/* Y */ static const uint8_t Font_Unifont_Regular_16_glyph_89[] = { 7, 10, 7, 0, 10, 131, 5, 18, 34, 130, 4, 8, 16, 32 };
|
||||
/* Z */ static const uint8_t Font_Unifont_Regular_16_glyph_90[] = { 6, 10, 7, 0, 10, 252, 16, 66, 16, 132, 32, 131, 240 };
|
||||
/* [ */ static const uint8_t Font_Unifont_Regular_16_glyph_91[] = { 3, 12, 7, 3, 11, 242, 73, 36, 146, 112 };
|
||||
/* \ */ static const uint8_t Font_Unifont_Regular_16_glyph_92[] = { 6, 10, 7, 0, 10, 130, 4, 8, 32, 65, 2, 4, 16 };
|
||||
/* ] */ static const uint8_t Font_Unifont_Regular_16_glyph_93[] = { 3, 12, 7, 0, 11, 228, 146, 73, 36, 240 };
|
||||
/* ^ */ static const uint8_t Font_Unifont_Regular_16_glyph_94[] = { 6, 3, 7, 0, 12, 49, 40, 64 };
|
||||
/* _ */ static const uint8_t Font_Unifont_Regular_16_glyph_95[] = { 7, 1, 7, 0, 0, 254 };
|
||||
/* ` */ static const uint8_t Font_Unifont_Regular_16_glyph_96[] = { 3, 3, 7, 1, 13, 136, 128 };
|
||||
/* a */ static const uint8_t Font_Unifont_Regular_16_glyph_97[] = { 6, 8, 7, 0, 8, 122, 16, 95, 134, 24, 221, 0 };
|
||||
/* b */ static const uint8_t Font_Unifont_Regular_16_glyph_98[] = { 6, 11, 7, 0, 11, 130, 8, 46, 198, 24, 97, 135, 27, 128 };
|
||||
/* c */ static const uint8_t Font_Unifont_Regular_16_glyph_99[] = { 6, 8, 7, 0, 8, 122, 24, 32, 130, 8, 94, 0 };
|
||||
/* d */ static const uint8_t Font_Unifont_Regular_16_glyph_100[] = { 6, 11, 7, 0, 11, 4, 16, 93, 142, 24, 97, 134, 55, 64 };
|
||||
/* e */ static const uint8_t Font_Unifont_Regular_16_glyph_101[] = { 6, 8, 7, 0, 8, 122, 24, 127, 130, 8, 94, 0 };
|
||||
/* f */ static const uint8_t Font_Unifont_Regular_16_glyph_102[] = { 5, 11, 7, 0, 11, 25, 8, 79, 144, 132, 33, 8 };
|
||||
/* g */ static const uint8_t Font_Unifont_Regular_16_glyph_103[] = { 6, 11, 7, 0, 9, 5, 216, 162, 137, 196, 30, 134, 23, 128 };
|
||||
/* h */ static const uint8_t Font_Unifont_Regular_16_glyph_104[] = { 6, 11, 7, 0, 11, 130, 8, 46, 198, 24, 97, 134, 24, 64 };
|
||||
/* i */ static const uint8_t Font_Unifont_Regular_16_glyph_105[] = { 5, 11, 7, 1, 11, 33, 0, 194, 16, 132, 33, 62 };
|
||||
/* j */ static const uint8_t Font_Unifont_Regular_16_glyph_106[] = { 5, 13, 7, 0, 11, 8, 64, 48, 132, 33, 8, 67, 38, 0 };
|
||||
/* k */ static const uint8_t Font_Unifont_Regular_16_glyph_107[] = { 6, 11, 7, 0, 11, 130, 8, 34, 146, 140, 40, 146, 40, 64 };
|
||||
/* l */ static const uint8_t Font_Unifont_Regular_16_glyph_108[] = { 5, 11, 7, 1, 11, 97, 8, 66, 16, 132, 33, 62 };
|
||||
/* m */ static const uint8_t Font_Unifont_Regular_16_glyph_109[] = { 7, 8, 7, 0, 8, 237, 38, 76, 153, 50, 100, 201, 0 };
|
||||
/* n */ static const uint8_t Font_Unifont_Regular_16_glyph_110[] = { 6, 8, 7, 0, 8, 187, 24, 97, 134, 24, 97, 0 };
|
||||
/* o */ static const uint8_t Font_Unifont_Regular_16_glyph_111[] = { 6, 8, 7, 0, 8, 122, 24, 97, 134, 24, 94, 0 };
|
||||
/* p */ static const uint8_t Font_Unifont_Regular_16_glyph_112[] = { 6, 10, 7, 0, 8, 187, 24, 97, 134, 28, 110, 130, 0 };
|
||||
/* q */ static const uint8_t Font_Unifont_Regular_16_glyph_113[] = { 6, 10, 7, 0, 8, 118, 56, 97, 134, 24, 221, 4, 16 };
|
||||
/* r */ static const uint8_t Font_Unifont_Regular_16_glyph_114[] = { 6, 8, 7, 0, 8, 187, 24, 96, 130, 8, 32, 0 };
|
||||
/* s */ static const uint8_t Font_Unifont_Regular_16_glyph_115[] = { 6, 8, 7, 0, 8, 122, 24, 24, 24, 24, 94, 0 };
|
||||
/* t */ static const uint8_t Font_Unifont_Regular_16_glyph_116[] = { 5, 10, 7, 0, 10, 33, 9, 242, 16, 132, 32, 192 };
|
||||
/* u */ static const uint8_t Font_Unifont_Regular_16_glyph_117[] = { 6, 8, 7, 0, 8, 134, 24, 97, 134, 24, 221, 0 };
|
||||
/* v */ static const uint8_t Font_Unifont_Regular_16_glyph_118[] = { 6, 8, 7, 0, 8, 134, 24, 82, 73, 35, 12, 0 };
|
||||
/* w */ static const uint8_t Font_Unifont_Regular_16_glyph_119[] = { 7, 8, 7, 0, 8, 131, 38, 76, 153, 50, 100, 182, 0 };
|
||||
/* x */ static const uint8_t Font_Unifont_Regular_16_glyph_120[] = { 6, 8, 7, 0, 8, 134, 20, 140, 49, 40, 97, 0 };
|
||||
/* y */ static const uint8_t Font_Unifont_Regular_16_glyph_121[] = { 6, 10, 7, 0, 8, 134, 24, 97, 133, 51, 65, 5, 224 };
|
||||
/* z */ static const uint8_t Font_Unifont_Regular_16_glyph_122[] = { 6, 8, 7, 0, 8, 252, 16, 132, 33, 8, 63, 0 };
|
||||
/* { */ static const uint8_t Font_Unifont_Regular_16_glyph_123[] = { 4, 13, 7, 1, 11, 52, 66, 36, 132, 34, 68, 48 };
|
||||
/* | */ static const uint8_t Font_Unifont_Regular_16_glyph_124[] = { 1, 14, 7, 3, 12, 255, 252 };
|
||||
/* } */ static const uint8_t Font_Unifont_Regular_16_glyph_125[] = { 4, 13, 7, 1, 11, 194, 36, 66, 18, 68, 34, 192 };
|
||||
/* ~ */ static const uint8_t Font_Unifont_Regular_16_glyph_126[] = { 7, 3, 7, 0, 11, 99, 38, 48 };
|
||||
|
||||
const uint8_t Font_Unifont_Regular_16_glyph_nonprintable[] = { 6, 10, 7, 0, 10, 133, 231, 190, 247, 190, 255, 239, 191 };
|
||||
|
||||
const uint8_t * const Font_Unifont_Regular_16[126 + 1 - 32] = {
|
||||
Font_Unifont_Regular_16_glyph_32,
|
||||
Font_Unifont_Regular_16_glyph_33,
|
||||
Font_Unifont_Regular_16_glyph_34,
|
||||
Font_Unifont_Regular_16_glyph_35,
|
||||
Font_Unifont_Regular_16_glyph_36,
|
||||
Font_Unifont_Regular_16_glyph_37,
|
||||
Font_Unifont_Regular_16_glyph_38,
|
||||
Font_Unifont_Regular_16_glyph_39,
|
||||
Font_Unifont_Regular_16_glyph_40,
|
||||
Font_Unifont_Regular_16_glyph_41,
|
||||
Font_Unifont_Regular_16_glyph_42,
|
||||
Font_Unifont_Regular_16_glyph_43,
|
||||
Font_Unifont_Regular_16_glyph_44,
|
||||
Font_Unifont_Regular_16_glyph_45,
|
||||
Font_Unifont_Regular_16_glyph_46,
|
||||
Font_Unifont_Regular_16_glyph_47,
|
||||
Font_Unifont_Regular_16_glyph_48,
|
||||
Font_Unifont_Regular_16_glyph_49,
|
||||
Font_Unifont_Regular_16_glyph_50,
|
||||
Font_Unifont_Regular_16_glyph_51,
|
||||
Font_Unifont_Regular_16_glyph_52,
|
||||
Font_Unifont_Regular_16_glyph_53,
|
||||
Font_Unifont_Regular_16_glyph_54,
|
||||
Font_Unifont_Regular_16_glyph_55,
|
||||
Font_Unifont_Regular_16_glyph_56,
|
||||
Font_Unifont_Regular_16_glyph_57,
|
||||
Font_Unifont_Regular_16_glyph_58,
|
||||
Font_Unifont_Regular_16_glyph_59,
|
||||
Font_Unifont_Regular_16_glyph_60,
|
||||
Font_Unifont_Regular_16_glyph_61,
|
||||
Font_Unifont_Regular_16_glyph_62,
|
||||
Font_Unifont_Regular_16_glyph_63,
|
||||
Font_Unifont_Regular_16_glyph_64,
|
||||
Font_Unifont_Regular_16_glyph_65,
|
||||
Font_Unifont_Regular_16_glyph_66,
|
||||
Font_Unifont_Regular_16_glyph_67,
|
||||
Font_Unifont_Regular_16_glyph_68,
|
||||
Font_Unifont_Regular_16_glyph_69,
|
||||
Font_Unifont_Regular_16_glyph_70,
|
||||
Font_Unifont_Regular_16_glyph_71,
|
||||
Font_Unifont_Regular_16_glyph_72,
|
||||
Font_Unifont_Regular_16_glyph_73,
|
||||
Font_Unifont_Regular_16_glyph_74,
|
||||
Font_Unifont_Regular_16_glyph_75,
|
||||
Font_Unifont_Regular_16_glyph_76,
|
||||
Font_Unifont_Regular_16_glyph_77,
|
||||
Font_Unifont_Regular_16_glyph_78,
|
||||
Font_Unifont_Regular_16_glyph_79,
|
||||
Font_Unifont_Regular_16_glyph_80,
|
||||
Font_Unifont_Regular_16_glyph_81,
|
||||
Font_Unifont_Regular_16_glyph_82,
|
||||
Font_Unifont_Regular_16_glyph_83,
|
||||
Font_Unifont_Regular_16_glyph_84,
|
||||
Font_Unifont_Regular_16_glyph_85,
|
||||
Font_Unifont_Regular_16_glyph_86,
|
||||
Font_Unifont_Regular_16_glyph_87,
|
||||
Font_Unifont_Regular_16_glyph_88,
|
||||
Font_Unifont_Regular_16_glyph_89,
|
||||
Font_Unifont_Regular_16_glyph_90,
|
||||
Font_Unifont_Regular_16_glyph_91,
|
||||
Font_Unifont_Regular_16_glyph_92,
|
||||
Font_Unifont_Regular_16_glyph_93,
|
||||
Font_Unifont_Regular_16_glyph_94,
|
||||
Font_Unifont_Regular_16_glyph_95,
|
||||
Font_Unifont_Regular_16_glyph_96,
|
||||
Font_Unifont_Regular_16_glyph_97,
|
||||
Font_Unifont_Regular_16_glyph_98,
|
||||
Font_Unifont_Regular_16_glyph_99,
|
||||
Font_Unifont_Regular_16_glyph_100,
|
||||
Font_Unifont_Regular_16_glyph_101,
|
||||
Font_Unifont_Regular_16_glyph_102,
|
||||
Font_Unifont_Regular_16_glyph_103,
|
||||
Font_Unifont_Regular_16_glyph_104,
|
||||
Font_Unifont_Regular_16_glyph_105,
|
||||
Font_Unifont_Regular_16_glyph_106,
|
||||
Font_Unifont_Regular_16_glyph_107,
|
||||
Font_Unifont_Regular_16_glyph_108,
|
||||
Font_Unifont_Regular_16_glyph_109,
|
||||
Font_Unifont_Regular_16_glyph_110,
|
||||
Font_Unifont_Regular_16_glyph_111,
|
||||
Font_Unifont_Regular_16_glyph_112,
|
||||
Font_Unifont_Regular_16_glyph_113,
|
||||
Font_Unifont_Regular_16_glyph_114,
|
||||
Font_Unifont_Regular_16_glyph_115,
|
||||
Font_Unifont_Regular_16_glyph_116,
|
||||
Font_Unifont_Regular_16_glyph_117,
|
||||
Font_Unifont_Regular_16_glyph_118,
|
||||
Font_Unifont_Regular_16_glyph_119,
|
||||
Font_Unifont_Regular_16_glyph_120,
|
||||
Font_Unifont_Regular_16_glyph_121,
|
||||
Font_Unifont_Regular_16_glyph_122,
|
||||
Font_Unifont_Regular_16_glyph_123,
|
||||
Font_Unifont_Regular_16_glyph_124,
|
||||
Font_Unifont_Regular_16_glyph_125,
|
||||
Font_Unifont_Regular_16_glyph_126,
|
||||
};
|
7
core/embed/extmod/modtrezorui/font_unifont_regular_16.h
Normal file
@ -0,0 +1,7 @@
|
||||
#include <stdint.h>
|
||||
|
||||
#if TREZOR_FONT_BPP != 1
|
||||
#error Wrong TREZOR_FONT_BPP (expected 1)
|
||||
#endif
|
||||
extern const uint8_t* const Font_Unifont_Regular_16[126 + 1 - 32];
|
||||
extern const uint8_t Font_Unifont_Regular_16_glyph_nonprintable[];
|
@ -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 };
|
||||
|
@ -16,6 +16,8 @@ static void _librust_qstrs(void) {
|
||||
MP_QSTR_CONFIRMED;
|
||||
MP_QSTR_CANCELLED;
|
||||
MP_QSTR_INFO;
|
||||
MP_QSTR_checked_index;
|
||||
MP_QSTR_choices;
|
||||
MP_QSTR_confirm_action;
|
||||
MP_QSTR_confirm_blob;
|
||||
MP_QSTR_confirm_properties;
|
||||
@ -23,38 +25,50 @@ static void _librust_qstrs(void) {
|
||||
MP_QSTR_confirm_joint_total;
|
||||
MP_QSTR_confirm_modify_fee;
|
||||
MP_QSTR_confirm_modify_output;
|
||||
MP_QSTR_confirm_output;
|
||||
MP_QSTR_confirm_output_r;
|
||||
MP_QSTR_confirm_payment_request;
|
||||
MP_QSTR_confirm_reset_device;
|
||||
MP_QSTR_confirm_recovery;
|
||||
MP_QSTR_confirm_text;
|
||||
MP_QSTR_confirm_total;
|
||||
MP_QSTR_confirm_total_r;
|
||||
MP_QSTR_confirm_value;
|
||||
MP_QSTR_confirm_with_info;
|
||||
MP_QSTR_confirm_recovery;
|
||||
MP_QSTR_show_checklist;
|
||||
MP_QSTR_show_error;
|
||||
MP_QSTR_show_qr;
|
||||
MP_QSTR_show_success;
|
||||
MP_QSTR_show_warning;
|
||||
MP_QSTR_show_info;
|
||||
MP_QSTR_show_simple;
|
||||
MP_QSTR_request_number;
|
||||
MP_QSTR_request_pin;
|
||||
MP_QSTR_request_passphrase;
|
||||
MP_QSTR_confirm_word;
|
||||
MP_QSTR_pin_confirm_action;
|
||||
MP_QSTR_request_bip39;
|
||||
MP_QSTR_request_number;
|
||||
MP_QSTR_request_passphrase;
|
||||
MP_QSTR_request_pin;
|
||||
MP_QSTR_request_slip39;
|
||||
MP_QSTR_select_word;
|
||||
MP_QSTR_select_word_count;
|
||||
MP_QSTR_share_words;
|
||||
MP_QSTR_show_checklist;
|
||||
MP_QSTR_show_error;
|
||||
MP_QSTR_show_group_share_success;
|
||||
MP_QSTR_show_info;
|
||||
MP_QSTR_show_qr;
|
||||
MP_QSTR_show_remaining_shares;
|
||||
MP_QSTR_show_success;
|
||||
MP_QSTR_show_simple;
|
||||
MP_QSTR_show_warning;
|
||||
MP_QSTR_show_share_words;
|
||||
MP_QSTR_request_word_count;
|
||||
MP_QSTR_request_word_bip39;
|
||||
MP_QSTR_tutorial;
|
||||
|
||||
MP_QSTR_attach_timer_fn;
|
||||
MP_QSTR_touch_event;
|
||||
MP_QSTR_button_event;
|
||||
MP_QSTR_timer;
|
||||
MP_QSTR_paint;
|
||||
MP_QSTR_request_complete_repaint;
|
||||
MP_QSTR_trace;
|
||||
MP_QSTR_bounds;
|
||||
MP_QSTR_button_event;
|
||||
MP_QSTR_page_count;
|
||||
MP_QSTR_paint;
|
||||
MP_QSTR_place;
|
||||
MP_QSTR_request_complete_repaint;
|
||||
MP_QSTR_touch_event;
|
||||
MP_QSTR_timer;
|
||||
MP_QSTR_trace;
|
||||
|
||||
MP_QSTR_title;
|
||||
MP_QSTR_subtitle;
|
||||
@ -90,4 +104,9 @@ static void _librust_qstrs(void) {
|
||||
MP_QSTR_active;
|
||||
MP_QSTR_info_button;
|
||||
MP_QSTR_time_ms;
|
||||
MP_QSTR_fee_amount;
|
||||
MP_QSTR_fee_rate_amount;
|
||||
MP_QSTR_total_label;
|
||||
MP_QSTR_fee_label;
|
||||
MP_QSTR_truncated_address;
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ use super::ffi;
|
||||
///
|
||||
/// Given the above assumptions about MicroPython strings, working with
|
||||
/// StrBuffers in Rust is safe.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StrBuffer {
|
||||
ptr: *const u8,
|
||||
len: usize,
|
||||
|
@ -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(">")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -44,6 +44,25 @@ pub fn word_completion_mask(prefix: &str) -> u32 {
|
||||
unsafe { ffi::mnemonic_word_completion_mask(prefix.as_ptr() as _, prefix.len() as _) }
|
||||
}
|
||||
|
||||
/// Returns all possible letters that form a valid word together with some
|
||||
/// prefix.
|
||||
pub fn get_available_letters(prefix: &str) -> impl Iterator<Item = char> {
|
||||
const CHARS: [char; 26] = [
|
||||
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r',
|
||||
's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
|
||||
];
|
||||
|
||||
let mask = word_completion_mask(prefix);
|
||||
CHARS
|
||||
.iter()
|
||||
.filter(move |ch| bitmask_from_char(ch) & mask != 0)
|
||||
.copied()
|
||||
}
|
||||
|
||||
fn bitmask_from_char(ch: &char) -> u32 {
|
||||
1 << (*ch as u8 - b'a')
|
||||
}
|
||||
|
||||
pub struct Wordlist(&'static [*const cty::c_char]);
|
||||
|
||||
impl Wordlist {
|
||||
@ -190,4 +209,35 @@ mod tests {
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(result, expected_result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_word_completion_mask() {
|
||||
let result = word_completion_mask("ab");
|
||||
assert_eq!(result, 0b101000100100100000001);
|
||||
let result = word_completion_mask("zoo");
|
||||
assert_eq!(result, 0b0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_available_letters() {
|
||||
let expected_result = vec!['a', 'i', 'l', 'o', 's', 'u'];
|
||||
let result = get_available_letters("ab").collect::<Vec<_>>();
|
||||
assert_eq!(result, expected_result);
|
||||
|
||||
let expected_result = vec!['a', 'e', 'i', 'o', 'u'];
|
||||
let result = get_available_letters("str").collect::<Vec<_>>();
|
||||
assert_eq!(result, expected_result);
|
||||
|
||||
let result = get_available_letters("zoo").collect::<Vec<_>>();
|
||||
assert_eq!(result.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bitmask_from_char() {
|
||||
assert_eq!(bitmask_from_char(&'a'), 0b1);
|
||||
assert_eq!(bitmask_from_char(&'b'), 0b10);
|
||||
assert_eq!(bitmask_from_char(&'c'), 0b100);
|
||||
assert_eq!(bitmask_from_char(&'m'), 0b1000000000000);
|
||||
assert_eq!(bitmask_from_char(&'z'), 0b10000000000000000000000000);
|
||||
}
|
||||
}
|
||||
|
@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -83,6 +83,10 @@ impl<T> Child<T> {
|
||||
self.component
|
||||
}
|
||||
|
||||
pub fn inner_mut(&mut self) -> &mut T {
|
||||
&mut self.component
|
||||
}
|
||||
|
||||
/// Access inner component mutably, track whether a paint call has been
|
||||
/// requested, and propagate the flag upwards the component tree.
|
||||
pub fn mutate<F, U>(&mut self, ctx: &mut EventCtx, component_func: F) -> U
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -14,7 +14,9 @@ pub enum PageMsg<T, U> {
|
||||
}
|
||||
|
||||
pub trait Paginate {
|
||||
/// How many pages of content are there in total?
|
||||
fn page_count(&mut self) -> usize;
|
||||
/// Navigate to the given page.
|
||||
fn change_page(&mut self, active_page: usize);
|
||||
}
|
||||
|
||||
@ -27,6 +29,11 @@ where
|
||||
let mut page_count = 1; // There's always at least one page.
|
||||
let mut char_offset = 0;
|
||||
|
||||
// Make sure we're starting from the beginning.
|
||||
self.set_char_offset(char_offset);
|
||||
|
||||
// Looping through the content and counting pages
|
||||
// until we finally fit.
|
||||
loop {
|
||||
let fit = self.layout_content(&mut TextNoOp);
|
||||
match fit {
|
||||
@ -57,6 +64,8 @@ where
|
||||
// Make sure we're starting from the beginning.
|
||||
self.set_char_offset(char_offset);
|
||||
|
||||
// Looping through the content until we arrive at
|
||||
// the wanted page.
|
||||
while active_page < to_page {
|
||||
let fit = self.layout_content(&mut TextNoOp);
|
||||
match fit {
|
||||
|
105
core/embed/rust/src/ui/component/text/common.rs
Normal file
@ -0,0 +1,105 @@
|
||||
use crate::ui::{component::EventCtx, util::ResultExt};
|
||||
use heapless::String;
|
||||
|
||||
/// Reified editing operations of `TextBox`.
|
||||
///
|
||||
/// Note: This does not contain all supported editing operations, only the ones
|
||||
/// we currently use.
|
||||
pub enum TextEdit {
|
||||
ReplaceLast(char),
|
||||
Append(char),
|
||||
}
|
||||
|
||||
/// Wraps a character buffer of maximum length `L` and provides text editing
|
||||
/// operations over it. Text ops usually take a `EventCtx` to request a paint
|
||||
/// pass in case of any state modification.
|
||||
pub struct TextBox<const L: usize> {
|
||||
text: String<L>,
|
||||
}
|
||||
|
||||
impl<const L: usize> TextBox<L> {
|
||||
/// Create a new `TextBox` with content `text`.
|
||||
pub fn new(text: String<L>) -> Self {
|
||||
Self { text }
|
||||
}
|
||||
|
||||
/// Create an empty `TextBox`.
|
||||
pub fn empty() -> Self {
|
||||
Self::new(String::new())
|
||||
}
|
||||
|
||||
pub fn content(&self) -> &str {
|
||||
&self.text
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.text.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.text.is_empty()
|
||||
}
|
||||
|
||||
pub fn is_full(&self) -> bool {
|
||||
self.text.len() == self.text.capacity()
|
||||
}
|
||||
|
||||
/// Delete the last character of content, if any.
|
||||
pub fn delete_last(&mut self, ctx: &mut EventCtx) {
|
||||
let changed = self.text.pop().is_some();
|
||||
if changed {
|
||||
ctx.request_paint();
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces the last character of the content with `ch`. If the content is
|
||||
/// empty, `ch` is appended.
|
||||
pub fn replace_last(&mut self, ctx: &mut EventCtx, ch: char) {
|
||||
let previous = self.text.pop();
|
||||
self.text
|
||||
.push(ch)
|
||||
.assert_if_debugging_ui("TextBox has zero capacity");
|
||||
let changed = previous != Some(ch);
|
||||
if changed {
|
||||
ctx.request_paint();
|
||||
}
|
||||
}
|
||||
|
||||
/// Append `ch` at the end of the content.
|
||||
pub fn append(&mut self, ctx: &mut EventCtx, ch: char) {
|
||||
self.text.push(ch).assert_if_debugging_ui("TextBox is full");
|
||||
ctx.request_paint();
|
||||
}
|
||||
|
||||
/// Append `slice` at the end of the content.
|
||||
pub fn append_slice(&mut self, ctx: &mut EventCtx, slice: &str) {
|
||||
self.text
|
||||
.push_str(slice)
|
||||
.assert_if_debugging_ui("TextBox is full");
|
||||
ctx.request_paint();
|
||||
}
|
||||
|
||||
/// Replace the textbox content with `text`.
|
||||
pub fn replace(&mut self, ctx: &mut EventCtx, text: &str) {
|
||||
if self.text != text {
|
||||
self.text.clear();
|
||||
self.text
|
||||
.push_str(text)
|
||||
.assert_if_debugging_ui("TextBox is full");
|
||||
ctx.request_paint();
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear the textbox content.
|
||||
pub fn clear(&mut self, ctx: &mut EventCtx) {
|
||||
self.replace(ctx, "");
|
||||
}
|
||||
|
||||
/// Apply a editing operation to the text buffer.
|
||||
pub fn apply(&mut self, ctx: &mut EventCtx, edit: TextEdit) {
|
||||
match edit {
|
||||
TextEdit::ReplaceLast(char) => self.replace_last(ctx, char),
|
||||
TextEdit::Append(char) => self.append(ctx, char),
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -106,18 +106,28 @@ impl TextLayout {
|
||||
self
|
||||
}
|
||||
|
||||
/// Baseline `Point` where we are starting to draw the text.
|
||||
pub fn initial_cursor(&self) -> Point {
|
||||
self.bounds.top_left() + Offset::y(self.style.text_font.text_height() + self.padding_top)
|
||||
}
|
||||
|
||||
/// Trying to fit the content on the current screen.
|
||||
pub fn fit_text(&self, text: &str) -> LayoutFit {
|
||||
self.layout_text(text, &mut self.initial_cursor(), &mut TextNoOp)
|
||||
}
|
||||
|
||||
/// Draw as much text as possible on the current screen.
|
||||
pub fn render_text(&self, text: &str) {
|
||||
self.layout_text(text, &mut self.initial_cursor(), &mut TextRenderer);
|
||||
}
|
||||
|
||||
/// Y coordinate of the bottom of the available space/bounds
|
||||
pub fn bottom_y(&self) -> i16 {
|
||||
(self.bounds.y1 - self.padding_bottom).max(self.bounds.y0)
|
||||
}
|
||||
|
||||
/// Perform some operations defined on `Op` for a list of those `Op`s
|
||||
/// - e.g. changing the color, changing the font or rendering the text.
|
||||
pub fn layout_ops<'o>(
|
||||
mut self,
|
||||
ops: &mut dyn Iterator<Item = Op<'o>>,
|
||||
@ -129,12 +139,16 @@ impl TextLayout {
|
||||
|
||||
for op in ops {
|
||||
match op {
|
||||
// Changing color
|
||||
Op::Color(color) => {
|
||||
self.style.text_color = color;
|
||||
}
|
||||
// Changing font
|
||||
Op::Font(font) => {
|
||||
self.style.text_font = font;
|
||||
}
|
||||
// Text - try to fit it on the current page and if it doesn't
|
||||
// fit, return the appropriate OutOfBounds message
|
||||
Op::Text(text) => match self.layout_text(text, cursor, sink) {
|
||||
LayoutFit::Fitting {
|
||||
processed_chars, ..
|
||||
@ -161,6 +175,9 @@ impl TextLayout {
|
||||
}
|
||||
}
|
||||
|
||||
/// Loop through the `text` and try to fit it on the current screen,
|
||||
/// reporting events to `sink`, which may do something with them (e.g. draw
|
||||
/// on screen).
|
||||
pub fn layout_text(
|
||||
&self,
|
||||
text: &str,
|
||||
@ -168,11 +185,10 @@ impl TextLayout {
|
||||
sink: &mut dyn LayoutSink,
|
||||
) -> LayoutFit {
|
||||
let init_cursor = *cursor;
|
||||
let bottom = (self.bounds.y1 - self.padding_bottom).max(self.bounds.y0);
|
||||
let mut remaining_text = text;
|
||||
|
||||
// Check if bounding box is high enough for at least one line.
|
||||
if cursor.y > bottom {
|
||||
if cursor.y > self.bottom_y() {
|
||||
sink.out_of_bounds();
|
||||
return LayoutFit::OutOfBounds {
|
||||
processed_chars: 0,
|
||||
@ -196,7 +212,12 @@ impl TextLayout {
|
||||
};
|
||||
|
||||
// Report the span at the cursor position.
|
||||
sink.text(*cursor, self, &remaining_text[..span.length]);
|
||||
// Not doing it when the span length is 0, as that
|
||||
// means we encountered a newline/line-break, which we do not draw.
|
||||
// Line-breaks are reported later.
|
||||
if span.length > 0 {
|
||||
sink.text(*cursor, self, &remaining_text[..span.length]);
|
||||
}
|
||||
|
||||
// Continue with the rest of the remaining_text.
|
||||
remaining_text = &remaining_text[span.length + span.skip_next_chars..];
|
||||
@ -212,7 +233,7 @@ impl TextLayout {
|
||||
sink.hyphen(*cursor, self);
|
||||
}
|
||||
// Check the amount of vertical space we have left.
|
||||
if cursor.y + span.advance.y > bottom {
|
||||
if cursor.y + span.advance.y > self.bottom_y() {
|
||||
if !remaining_text.is_empty() {
|
||||
// Append ellipsis to indicate more content is available, but only if we
|
||||
// haven't already appended a hyphen.
|
||||
@ -252,6 +273,7 @@ impl TextLayout {
|
||||
}
|
||||
}
|
||||
|
||||
/// Overall height of the content, including paddings.
|
||||
fn layout_height(&self, init_cursor: Point, end_cursor: Point) -> i16 {
|
||||
self.padding_top
|
||||
+ self.style.text_font.text_height()
|
||||
@ -270,6 +292,8 @@ impl Dimensions for TextLayout {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether we can fit content on the current screen.
|
||||
/// Knows how many characters got processed and how high the content is.
|
||||
pub enum LayoutFit {
|
||||
/// Entire content fits. Vertical size is returned in `height`.
|
||||
Fitting { processed_chars: usize, height: i16 },
|
||||
@ -278,6 +302,7 @@ pub enum LayoutFit {
|
||||
}
|
||||
|
||||
impl LayoutFit {
|
||||
/// How high is the processed/fitted content.
|
||||
pub fn height(&self) -> i16 {
|
||||
match self {
|
||||
LayoutFit::Fitting { height, .. } => *height,
|
||||
@ -286,19 +311,33 @@ impl LayoutFit {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: LayoutSink could support even things like drawing icons
|
||||
// or making custom x or y offsets from any position
|
||||
|
||||
/// Visitor for text segment operations.
|
||||
/// Defines responses for certain kind of events encountered
|
||||
/// when processing the content.
|
||||
pub trait LayoutSink {
|
||||
/// Text should be processed.
|
||||
fn text(&mut self, _cursor: Point, _layout: &TextLayout, _text: &str) {}
|
||||
/// Hyphen at the end of line.
|
||||
fn hyphen(&mut self, _cursor: Point, _layout: &TextLayout) {}
|
||||
/// Ellipsis at the end of the page.
|
||||
fn ellipsis(&mut self, _cursor: Point, _layout: &TextLayout) {}
|
||||
/// Line break - a newline.
|
||||
fn line_break(&mut self, _cursor: Point) {}
|
||||
/// Content cannot fit on the screen.
|
||||
fn out_of_bounds(&mut self) {}
|
||||
}
|
||||
|
||||
/// `LayoutSink` without any functionality.
|
||||
/// Used to consume events when counting pages
|
||||
/// or navigating to a certain page number.
|
||||
pub struct TextNoOp;
|
||||
|
||||
impl LayoutSink for TextNoOp {}
|
||||
|
||||
/// `LayoutSink` for rendering the content.
|
||||
pub struct TextRenderer;
|
||||
|
||||
impl LayoutSink for TextRenderer {
|
||||
@ -339,6 +378,7 @@ pub mod trace {
|
||||
|
||||
use super::*;
|
||||
|
||||
/// `LayoutSink` for debugging purposes.
|
||||
pub struct TraceSink<'a>(pub &'a mut dyn crate::trace::Tracer);
|
||||
|
||||
impl<'a> LayoutSink for TraceSink<'a> {
|
||||
@ -360,6 +400,7 @@ pub mod trace {
|
||||
}
|
||||
}
|
||||
|
||||
/// Operations that can be done on FormattedText.
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Op<'a> {
|
||||
/// Render text with current color and font.
|
||||
@ -371,6 +412,10 @@ pub enum Op<'a> {
|
||||
}
|
||||
|
||||
impl<'a> Op<'a> {
|
||||
/// Filtering the list of `Op`s to throw away all the content
|
||||
/// (text, font change or other operations) before a specific byte/character
|
||||
/// threshold. Used when showing the second, third... paginated page
|
||||
/// to skip the first one, two... pages.
|
||||
pub fn skip_n_text_bytes(
|
||||
ops: impl Iterator<Item = Op<'a>>,
|
||||
skip_bytes: usize,
|
||||
@ -392,19 +437,21 @@ impl<'a> Op<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Carries info about the content that was processed
|
||||
/// on the current line.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct Span {
|
||||
pub struct Span {
|
||||
/// How many characters from the input text this span is laying out.
|
||||
length: usize,
|
||||
pub length: usize,
|
||||
/// How many chars from the input text should we skip before fitting the
|
||||
/// next span?
|
||||
skip_next_chars: usize,
|
||||
pub skip_next_chars: usize,
|
||||
/// By how much to offset the cursor after this span. If the vertical offset
|
||||
/// is bigger than zero, it means we are breaking the line.
|
||||
advance: Offset,
|
||||
pub advance: Offset,
|
||||
/// If we are breaking the line, should we insert a hyphen right after this
|
||||
/// span to indicate a word-break?
|
||||
insert_hyphen_before_line_break: bool,
|
||||
pub insert_hyphen_before_line_break: bool,
|
||||
}
|
||||
|
||||
impl Span {
|
||||
@ -463,7 +510,7 @@ impl Span {
|
||||
}
|
||||
found_any_whitespace = true;
|
||||
} else if span_width + char_width > max_width {
|
||||
// Return the last breakpoint.
|
||||
// Cannot fit on this line. Return the last breakpoint.
|
||||
return line;
|
||||
} else {
|
||||
let have_space_for_break = span_width + char_width + hyphen_width <= max_width;
|
||||
@ -484,7 +531,7 @@ impl Span {
|
||||
span_width += char_width;
|
||||
}
|
||||
|
||||
// The whole text is fitting.
|
||||
// The whole text is fitting on the current line.
|
||||
Self {
|
||||
length: text.len(),
|
||||
advance: Offset::x(span_width),
|
||||
|
@ -1,3 +1,4 @@
|
||||
pub mod common;
|
||||
pub mod formatted;
|
||||
mod iter;
|
||||
pub mod layout;
|
||||
|
@ -239,11 +239,13 @@ pub mod trace {
|
||||
impl<T: ParagraphSource> crate::trace::Trace for Paragraphs<T> {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("Paragraphs");
|
||||
t.content_flag();
|
||||
for (layout, content) in Self::visible_content(&self.source, &self.visible, self.offset)
|
||||
{
|
||||
layout.layout_text(content, &mut layout.initial_cursor(), &mut TraceSink(t));
|
||||
t.string("\n");
|
||||
}
|
||||
t.content_flag();
|
||||
t.close();
|
||||
}
|
||||
}
|
||||
|
258
core/embed/rust/src/ui/debug.rs
Normal file
@ -0,0 +1,258 @@
|
||||
//! Including some useful debugging features,
|
||||
//! like printing of the struct details.
|
||||
|
||||
use heapless::String;
|
||||
|
||||
use super::{
|
||||
component::{
|
||||
pad::Pad,
|
||||
text::{
|
||||
common::TextBox,
|
||||
layout::{Span, TextLayout},
|
||||
},
|
||||
},
|
||||
display::{Color, Font, Icon},
|
||||
geometry::{Grid, Insets, Offset, Point, Rect},
|
||||
};
|
||||
use crate::{micropython::buffer::StrBuffer, time::Duration};
|
||||
|
||||
#[cfg(feature = "model_tr")]
|
||||
use super::model_tr::component::ButtonDetails;
|
||||
|
||||
// NOTE: not defining a common trait, like
|
||||
// Debug {fn print(&self);}, so that the trait does
|
||||
// not need to be imported when using the
|
||||
// print() function. It suits the use-case of being quickly
|
||||
// able to use the print() for debugging and then delete it.
|
||||
|
||||
/// TODO: find out how much storage these functions take
|
||||
/// and probably hide them behind debug feature
|
||||
|
||||
impl StrBuffer {
|
||||
pub fn print(&self) {
|
||||
println!("StrBuffer:: ", self.as_ref());
|
||||
}
|
||||
}
|
||||
|
||||
impl Duration {
|
||||
pub fn print(&self) {
|
||||
println!("Duration:: ", inttostr!(self.to_millis()));
|
||||
}
|
||||
}
|
||||
|
||||
impl Point {
|
||||
pub fn print(&self) {
|
||||
println!(
|
||||
"Point:: ",
|
||||
"x: ",
|
||||
inttostr!(self.x),
|
||||
", y: ",
|
||||
inttostr!(self.y)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
pub fn print(&self) {
|
||||
print!("Rect:: ");
|
||||
println!(&self.corners_points());
|
||||
}
|
||||
|
||||
pub fn corners_points(&self) -> String<30> {
|
||||
build_string!(
|
||||
30,
|
||||
"(",
|
||||
inttostr!(self.x0),
|
||||
",",
|
||||
inttostr!(self.y0),
|
||||
"), (",
|
||||
inttostr!(self.x1),
|
||||
",",
|
||||
inttostr!(self.y1),
|
||||
")"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for Rect {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("Rect");
|
||||
t.string(&self.corners_points());
|
||||
t.close();
|
||||
}
|
||||
}
|
||||
|
||||
impl Color {
|
||||
pub fn print(&self) {
|
||||
println!(
|
||||
"Color:: ",
|
||||
"R: ",
|
||||
inttostr!(self.r()),
|
||||
", G: ",
|
||||
inttostr!(self.g()),
|
||||
", B: ",
|
||||
inttostr!(self.b())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Font {
|
||||
pub fn print(&self) {
|
||||
println!("Font:: ", "text_height: ", inttostr!(self.text_height()));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "model_tr")]
|
||||
impl<T: Clone + AsRef<str>> ButtonDetails<T> {
|
||||
pub fn print(&self) {
|
||||
let text: String<20> = if let Some(text) = self.text.clone() {
|
||||
text.as_ref().into()
|
||||
} else {
|
||||
"None".into()
|
||||
};
|
||||
let icon_text: String<20> = if let Some(icon) = &self.icon {
|
||||
icon.text.into()
|
||||
} else {
|
||||
"None".into()
|
||||
};
|
||||
let force_width: String<20> = if let Some(force_width) = self.force_width {
|
||||
inttostr!(force_width).into()
|
||||
} else {
|
||||
"None".into()
|
||||
};
|
||||
println!(
|
||||
"ButtonDetails:: ",
|
||||
"text: ",
|
||||
text.as_ref(),
|
||||
", icon_text: ",
|
||||
icon_text.as_ref(),
|
||||
", with_outline: ",
|
||||
booltostr!(self.with_outline),
|
||||
", with_arms: ",
|
||||
booltostr!(self.with_arms),
|
||||
", force_width: ",
|
||||
force_width.as_ref()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Offset {
|
||||
pub fn print(&self) {
|
||||
println!(
|
||||
"Offset:: ",
|
||||
"x: ",
|
||||
inttostr!(self.x),
|
||||
", y: ",
|
||||
inttostr!(self.y)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Insets {
|
||||
pub fn print(&self) {
|
||||
println!(
|
||||
"Insets:: ",
|
||||
"top: ",
|
||||
inttostr!(self.top),
|
||||
", right: ",
|
||||
inttostr!(self.right),
|
||||
", bottom: ",
|
||||
inttostr!(self.bottom),
|
||||
", left: ",
|
||||
inttostr!(self.left)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Grid {
|
||||
pub fn print(&self) {
|
||||
print!(
|
||||
"Grid:: ",
|
||||
"rows: ",
|
||||
inttostr!(self.rows as i32),
|
||||
", cols: ",
|
||||
inttostr!(self.cols as i32),
|
||||
", spacing: ",
|
||||
inttostr!(self.spacing as i32)
|
||||
);
|
||||
print!(", area: ");
|
||||
self.area.print();
|
||||
}
|
||||
}
|
||||
|
||||
impl Icon {
|
||||
pub fn dimension_str(&self) -> String<10> {
|
||||
build_string!(
|
||||
10,
|
||||
inttostr!(self.width() as i32),
|
||||
"x",
|
||||
inttostr!(self.height() as i32)
|
||||
)
|
||||
}
|
||||
|
||||
pub fn print(&self) {
|
||||
println!(
|
||||
"Icon:: ",
|
||||
"text: ",
|
||||
self.text,
|
||||
", width: ",
|
||||
inttostr!(self.width() as i32),
|
||||
", height: ",
|
||||
inttostr!(self.height() as i32)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for Icon {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("Icon");
|
||||
t.string(self.text);
|
||||
t.string(&self.dimension_str());
|
||||
t.close();
|
||||
}
|
||||
}
|
||||
|
||||
impl TextLayout {
|
||||
pub fn print(&self) {
|
||||
print!(
|
||||
"TextLayout:: ",
|
||||
"padding_top: ",
|
||||
inttostr!(self.padding_top as i32),
|
||||
", padding_bottom: ",
|
||||
inttostr!(self.padding_bottom as i32)
|
||||
);
|
||||
print!(", bounds: ");
|
||||
self.bounds.print();
|
||||
}
|
||||
}
|
||||
|
||||
impl Span {
|
||||
pub fn print(&self) {
|
||||
print!(
|
||||
"Span:: ",
|
||||
"length: ",
|
||||
inttostr!(self.length as i32),
|
||||
", skip_next_chars: ",
|
||||
inttostr!(self.skip_next_chars as i32),
|
||||
", insert_hyphen_before_line_break: ",
|
||||
booltostr!(self.insert_hyphen_before_line_break)
|
||||
);
|
||||
print!(", advance: ");
|
||||
self.advance.print();
|
||||
}
|
||||
}
|
||||
|
||||
impl Pad {
|
||||
pub fn print(&self) {
|
||||
print!("Pad:: ", "area: ");
|
||||
self.area.print();
|
||||
}
|
||||
}
|
||||
|
||||
impl<const L: usize> TextBox<L> {
|
||||
pub fn print(&self) {
|
||||
println!("TextBox:: ", "content: ", self.content());
|
||||
}
|
||||
}
|
80
core/embed/rust/src/ui/display/color.rs
Normal file
@ -0,0 +1,80 @@
|
||||
use crate::ui::lerp::Lerp;
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub struct Color(u16);
|
||||
|
||||
impl Color {
|
||||
pub const fn from_u16(val: u16) -> Self {
|
||||
Self(val)
|
||||
}
|
||||
|
||||
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
|
||||
let r = (r as u16 & 0xF8) << 8;
|
||||
let g = (g as u16 & 0xFC) << 3;
|
||||
let b = (b as u16 & 0xF8) >> 3;
|
||||
Self(r | g | b)
|
||||
}
|
||||
|
||||
pub const fn luminance(self) -> u32 {
|
||||
((self.r() as u32 * 299) / 1000)
|
||||
+ (self.g() as u32 * 587) / 1000
|
||||
+ (self.b() as u32 * 114) / 1000
|
||||
}
|
||||
|
||||
pub const fn r(self) -> u8 {
|
||||
(self.0 >> 8) as u8 & 0xF8
|
||||
}
|
||||
|
||||
pub const fn g(self) -> u8 {
|
||||
(self.0 >> 3) as u8 & 0xFC
|
||||
}
|
||||
|
||||
pub const fn b(self) -> u8 {
|
||||
(self.0 << 3) as u8 & 0xF8
|
||||
}
|
||||
|
||||
pub fn to_u16(self) -> u16 {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn hi_byte(self) -> u8 {
|
||||
(self.to_u16() >> 8) as u8
|
||||
}
|
||||
|
||||
pub fn lo_byte(self) -> u8 {
|
||||
(self.to_u16() & 0xFF) as u8
|
||||
}
|
||||
|
||||
pub fn negate(self) -> Self {
|
||||
Self(!self.0)
|
||||
}
|
||||
|
||||
pub const fn white() -> Self {
|
||||
Self::rgb(255, 255, 255)
|
||||
}
|
||||
|
||||
pub const fn black() -> Self {
|
||||
Self::rgb(0, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Lerp for Color {
|
||||
fn lerp(a: Self, b: Self, t: f32) -> Self {
|
||||
let r = u8::lerp(a.r(), b.r(), t);
|
||||
let g = u8::lerp(a.g(), b.g(), t);
|
||||
let b = u8::lerp(a.b(), b.b(), t);
|
||||
Color::rgb(r, g, b)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u16> for Color {
|
||||
fn from(val: u16) -> Self {
|
||||
Self(val)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for u16 {
|
||||
fn from(val: Color) -> Self {
|
||||
val.to_u16()
|
||||
}
|
||||
}
|
176
core/embed/rust/src/ui/display/font.rs
Normal file
@ -0,0 +1,176 @@
|
||||
use crate::{ui::{constant, geometry::{Point, Offset, Rect}}, trezorhal::display};
|
||||
use core::slice;
|
||||
|
||||
use super::{Color, get_color_table, pixeldata, set_window, get_offset};
|
||||
|
||||
|
||||
pub struct Glyph {
|
||||
pub width: i16,
|
||||
pub height: i16,
|
||||
pub adv: i16,
|
||||
pub bearing_x: i16,
|
||||
pub bearing_y: i16,
|
||||
data: &'static [u8],
|
||||
}
|
||||
|
||||
impl Glyph {
|
||||
/// Construct a `Glyph` from a raw pointer.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// This function is unsafe because the caller has to guarantee that `data`
|
||||
/// is pointing to a memory containing a valid glyph data, that is:
|
||||
/// - contains valid glyph metadata
|
||||
/// - data has appropriate size
|
||||
/// - data must have static lifetime
|
||||
pub unsafe fn load(data: *const u8) -> Self {
|
||||
unsafe {
|
||||
let width = *data.offset(0) as i16;
|
||||
let height = *data.offset(1) as i16;
|
||||
|
||||
let data_bits = constant::FONT_BPP * width * height;
|
||||
|
||||
let data_bytes = if data_bits % 8 == 0 {
|
||||
data_bits / 8
|
||||
} else {
|
||||
(data_bits / 8) + 1
|
||||
};
|
||||
|
||||
Glyph {
|
||||
width,
|
||||
height,
|
||||
adv: *data.offset(2) as i16,
|
||||
bearing_x: *data.offset(3) as i16,
|
||||
bearing_y: *data.offset(4) as i16,
|
||||
data: slice::from_raw_parts(data.offset(5), data_bytes as usize),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print(&self, pos: Point, colortable: [Color; 16]) -> i16 {
|
||||
let bearing = Offset::new(self.bearing_x, -self.bearing_y);
|
||||
let size = Offset::new(self.width, self.height);
|
||||
let pos_adj = pos + bearing;
|
||||
let r = Rect::from_top_left_and_size(pos_adj, size);
|
||||
|
||||
let area = r.translate(get_offset());
|
||||
let window = area.clamp(constant::screen());
|
||||
|
||||
set_window(window);
|
||||
|
||||
for y in window.y0..window.y1 {
|
||||
for x in window.x0..window.x1 {
|
||||
let p = Point::new(x, y);
|
||||
let r = p - pos_adj;
|
||||
let c = self.get_pixel_data(r);
|
||||
pixeldata(colortable[c as usize]);
|
||||
}
|
||||
}
|
||||
self.adv
|
||||
}
|
||||
|
||||
/// Returns 0 (black) or 15 (white).
|
||||
pub fn unpack_bpp1(&self, a: i16) -> u8 {
|
||||
let c_data = self.data[(a / 8) as usize];
|
||||
((c_data >> (7 - (a % 8))) & 0x01) * 15
|
||||
}
|
||||
|
||||
pub fn unpack_bpp2(&self, a: i16) -> u8 {
|
||||
let c_data = self.data[(a / 4) as usize];
|
||||
((c_data >> (6 - (a % 4) * 2)) & 0x03) * 5
|
||||
}
|
||||
|
||||
pub fn unpack_bpp4(&self, a: i16) -> u8 {
|
||||
let c_data = self.data[(a / 2) as usize];
|
||||
(c_data >> (4 - (a % 2) * 4)) & 0x0F
|
||||
}
|
||||
|
||||
pub fn unpack_bpp8(&self, a: i16) -> u8 {
|
||||
let c_data = self.data[a as usize];
|
||||
c_data >> 4
|
||||
}
|
||||
|
||||
pub fn get_pixel_data(&self, p: Offset) -> u8 {
|
||||
let a = p.x + p.y * self.width;
|
||||
|
||||
match constant::FONT_BPP {
|
||||
1 => self.unpack_bpp1(a),
|
||||
2 => self.unpack_bpp2(a),
|
||||
4 => self.unpack_bpp4(a),
|
||||
8 => self.unpack_bpp8(a),
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Font constants. Keep in sync with FONT_ definitions in
|
||||
/// `extmod/modtrezorui/fonts/fonts.h`.
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum Font {
|
||||
NORMAL = 1,
|
||||
BOLD = 2,
|
||||
MONO = 3,
|
||||
DEMIBOLD = 5,
|
||||
}
|
||||
|
||||
impl From<Font> for i32 {
|
||||
fn from(font: Font) -> i32 {
|
||||
-(font as i32)
|
||||
}
|
||||
}
|
||||
|
||||
impl Font {
|
||||
pub fn text_width(self, text: &str) -> i16 {
|
||||
display::text_width(text, self.into()) as i16
|
||||
}
|
||||
|
||||
pub fn char_width(self, ch: char) -> i16 {
|
||||
display::char_width(ch, self.into()) as i16
|
||||
}
|
||||
|
||||
pub fn text_height(self) -> i16 {
|
||||
display::text_height(self.into()) as i16
|
||||
}
|
||||
|
||||
pub fn line_height(self) -> i16 {
|
||||
constant::LINE_SPACE + self.text_height()
|
||||
}
|
||||
|
||||
pub fn get_glyph(self, char_byte: u8) -> Option<Glyph> {
|
||||
let gl_data = display::get_char_glyph(char_byte, self.into());
|
||||
|
||||
if gl_data.is_null() {
|
||||
return None;
|
||||
}
|
||||
unsafe { Some(Glyph::load(gl_data)) }
|
||||
}
|
||||
|
||||
pub fn display_text(self, text: &str, baseline: Point, fg_color: Color, bg_color: Color) {
|
||||
let colortable = get_color_table(fg_color, bg_color);
|
||||
let mut adv_total = 0;
|
||||
for c in text.bytes() {
|
||||
let g = self.get_glyph(c);
|
||||
if let Some(gly) = g {
|
||||
let adv = gly.print(baseline + Offset::new(adv_total, 0), colortable);
|
||||
adv_total += adv;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the length of the longest suffix from a given `text`
|
||||
/// that will fit into the area `width` pixels wide.
|
||||
pub fn longest_suffix(self, width: i16, text: &str) -> usize {
|
||||
let mut text_width = 0;
|
||||
for (chars_from_right, c) in text.chars().rev().enumerate() {
|
||||
let c_width = self.char_width(c);
|
||||
if text_width + c_width > width {
|
||||
// Another character cannot be fitted, we're done.
|
||||
return chars_from_right;
|
||||
}
|
||||
text_width += c_width;
|
||||
}
|
||||
|
||||
text.len() // it fits in its entirety
|
||||
}
|
||||
}
|
97
core/embed/rust/src/ui/display/icon.rs
Normal file
@ -0,0 +1,97 @@
|
||||
use crate::{
|
||||
trezorhal::display::ToifFormat,
|
||||
ui::geometry::{Offset, Point, Rect},
|
||||
};
|
||||
|
||||
use super::{icon_rect, toif_info_ensure, Color};
|
||||
|
||||
/// Storing the icon together with its name
|
||||
/// Needs to be a tuple-struct, so it can be made `const`
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct IconAndName(&'static [u8], &'static str);
|
||||
|
||||
impl IconAndName {
|
||||
pub const fn new(icon: &'static [u8], name: &'static str) -> Self {
|
||||
Self(icon, name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Holding icon data and allowing it to draw itself.
|
||||
/// Lots of draw methods exist so that we can easily
|
||||
/// "glue" the icon together with other elements
|
||||
/// (text, display boundary, etc.) according to their position.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Icon {
|
||||
pub data: &'static [u8],
|
||||
// Text is useful for debugging purposes.
|
||||
pub text: &'static str,
|
||||
// TODO: could include the info about "real" icon dimensions,
|
||||
// accounting for the TOIF limitations (when we sometimes
|
||||
// need to have empty row or column) - it could be
|
||||
// erasing those empty rows/columns when we draw the icon.
|
||||
}
|
||||
|
||||
// TODO: consider merging it together with ToifInfo
|
||||
impl Icon {
|
||||
pub fn new(icon_and_name: IconAndName) -> Self {
|
||||
Icon {
|
||||
data: icon_and_name.0,
|
||||
text: icon_and_name.1,
|
||||
}
|
||||
}
|
||||
|
||||
fn toif_info(&self) -> (Offset, &[u8]) {
|
||||
toif_info_ensure(self.data, ToifFormat::GrayScaleEH)
|
||||
}
|
||||
|
||||
pub fn width(&self) -> i16 {
|
||||
self.toif_info().0.x as i16
|
||||
}
|
||||
|
||||
pub fn height(&self) -> i16 {
|
||||
self.toif_info().0.y as i16
|
||||
}
|
||||
|
||||
pub fn toif_size(&self) -> Offset {
|
||||
self.toif_info().0
|
||||
}
|
||||
|
||||
pub fn toif_data(&self) -> &[u8] {
|
||||
self.toif_info().1
|
||||
}
|
||||
|
||||
/// Display icon at a specified Rectangle.
|
||||
fn draw_icon_rect(&self, r: Rect, fg_color: Color, bg_color: Color) {
|
||||
icon_rect(r, self.toif_data(), fg_color, bg_color);
|
||||
}
|
||||
|
||||
/// Display the icon with left top baseline Point.
|
||||
pub fn draw_top_left(&self, baseline: Point, fg_color: Color, bg_color: Color) {
|
||||
let r = Rect::from_top_left_and_size(baseline, self.toif_size());
|
||||
self.draw_icon_rect(r, fg_color, bg_color);
|
||||
}
|
||||
|
||||
/// Display the icon with right top baseline Point.
|
||||
pub fn draw_top_right(&self, baseline: Point, fg_color: Color, bg_color: Color) {
|
||||
let r = Rect::from_top_right_and_size(baseline, self.toif_size());
|
||||
self.draw_icon_rect(r, fg_color, bg_color);
|
||||
}
|
||||
|
||||
/// Display the icon with right bottom baseline Point.
|
||||
pub fn draw_bottom_right(&self, baseline: Point, fg_color: Color, bg_color: Color) {
|
||||
let r = Rect::from_bottom_right_and_size(baseline, self.toif_size());
|
||||
self.draw_icon_rect(r, fg_color, bg_color);
|
||||
}
|
||||
|
||||
/// Display the icon with left bottom baseline Point.
|
||||
pub fn draw_bottom_left(&self, baseline: Point, fg_color: Color, bg_color: Color) {
|
||||
let r = Rect::from_bottom_left_and_size(baseline, self.toif_size());
|
||||
self.draw_icon_rect(r, fg_color, bg_color);
|
||||
}
|
||||
|
||||
/// Display the icon around center Point.
|
||||
pub fn draw_center(&self, center: Point, fg_color: Color, bg_color: Color) {
|
||||
let r = Rect::from_center_and_size(center, self.toif_size());
|
||||
self.draw_icon_rect(r, fg_color, bg_color);
|
||||
}
|
||||
}
|
@ -1,5 +1,8 @@
|
||||
#[cfg(any(feature = "model_tt", feature = "model_tr"))]
|
||||
pub mod loader;
|
||||
pub mod icon;
|
||||
pub mod font;
|
||||
pub mod color;
|
||||
|
||||
use super::{
|
||||
constant,
|
||||
@ -24,10 +27,13 @@ use crate::{
|
||||
},
|
||||
ui::lerp::Lerp,
|
||||
};
|
||||
use core::slice;
|
||||
|
||||
// Reexports
|
||||
#[cfg(any(feature = "model_tt", feature = "model_tr"))]
|
||||
pub use loader::{loader, loader_indeterminate, LOADER_MAX, LOADER_MIN};
|
||||
pub use icon::{Icon, IconAndName};
|
||||
pub use font::{Font, Glyph};
|
||||
pub use color::Color;
|
||||
|
||||
pub fn backlight() -> i32 {
|
||||
display::backlight(-1)
|
||||
@ -55,6 +61,7 @@ pub fn fade_backlight(target: i32) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fill a whole rectangle with a specific color.
|
||||
pub fn rect_fill(r: Rect, fg_color: Color) {
|
||||
display::bar(r.x0, r.y0, r.width(), r.height(), fg_color.into());
|
||||
}
|
||||
@ -66,17 +73,22 @@ pub fn rect_stroke(r: Rect, fg_color: Color) {
|
||||
display::bar(r.x0 + r.width() - 1, r.y0, 1, r.height(), fg_color.into());
|
||||
}
|
||||
|
||||
/// Draw a rectangle with rounded corners.
|
||||
pub fn rect_fill_rounded(r: Rect, fg_color: Color, bg_color: Color, radius: u8) {
|
||||
assert!([2, 4, 8, 16].iter().any(|allowed| radius == *allowed));
|
||||
display::bar_radius(
|
||||
r.x0,
|
||||
r.y0,
|
||||
r.width(),
|
||||
r.height(),
|
||||
fg_color.into(),
|
||||
bg_color.into(),
|
||||
radius,
|
||||
);
|
||||
if radius == 1 {
|
||||
rect_fill_rounded1(r, fg_color, bg_color);
|
||||
} else {
|
||||
assert!([2, 4, 8, 16].iter().any(|allowed| radius == *allowed));
|
||||
display::bar_radius(
|
||||
r.x0,
|
||||
r.y0,
|
||||
r.width(),
|
||||
r.height(),
|
||||
fg_color.into(),
|
||||
bg_color.into(),
|
||||
radius,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// NOTE: Cannot start at odd x-coordinate. In this case icon is shifted 1px
|
||||
@ -94,9 +106,15 @@ pub fn icon_top_left(top_left: Point, data: &[u8], fg_color: Color, bg_color: Co
|
||||
);
|
||||
}
|
||||
|
||||
/// Display icon given a center Point.
|
||||
pub fn icon(center: Point, data: &[u8], fg_color: Color, bg_color: Color) {
|
||||
let (toif_size, toif_data) = toif_info_ensure(data, ToifFormat::GrayScaleEH);
|
||||
let r = Rect::from_center_and_size(center, toif_size);
|
||||
icon_rect(r, toif_data, fg_color, bg_color);
|
||||
}
|
||||
|
||||
/// Display icon at a specified Rectangle, expects already sliced data without header.
|
||||
pub fn icon_rect(r: Rect, toif_data: &[u8], fg_color: Color, bg_color: Color) {
|
||||
display::icon(
|
||||
r.x0,
|
||||
r.y0,
|
||||
@ -179,35 +197,61 @@ fn toif_info_ensure(data: &[u8], format: ToifFormat) -> (Offset, &[u8]) {
|
||||
(size, payload)
|
||||
}
|
||||
|
||||
// Used on T1 only.
|
||||
/// Filling a rectangle with a rounding of 1 pixel - removing the corners.
|
||||
pub fn rect_fill_rounded1(r: Rect, fg_color: Color, bg_color: Color) {
|
||||
display::bar(r.x0, r.y0, r.width(), r.height(), fg_color.into());
|
||||
let corners = [
|
||||
r.top_left(),
|
||||
r.top_right() - Offset::x(1),
|
||||
r.bottom_right() - Offset::uniform(1),
|
||||
r.bottom_left() - Offset::y(1),
|
||||
];
|
||||
for p in corners.iter() {
|
||||
display::bar(p.x, p.y, 1, 1, bg_color.into());
|
||||
rect_fill(r, fg_color);
|
||||
rect_fill_corners(r, bg_color);
|
||||
}
|
||||
|
||||
/// Creating a rectangular outline with a given radius/rounding.
|
||||
pub fn rect_outline_rounded(r: Rect, fg_color: Color, bg_color: Color, radius: u8) {
|
||||
// Painting a bigger rectangle with FG and inner smaller with BG
|
||||
// to create the outline.
|
||||
let inner_r = r.shrink(1);
|
||||
if radius == 1 {
|
||||
rect_fill_rounded(r, fg_color, bg_color, 1);
|
||||
rect_fill(inner_r, bg_color);
|
||||
} else if radius == 2 {
|
||||
rect_fill_rounded(r, fg_color, bg_color, 2);
|
||||
rect_fill_rounded(inner_r, bg_color, fg_color, 1);
|
||||
} else if radius == 4 {
|
||||
rect_fill_rounded(r, fg_color, bg_color, 4);
|
||||
rect_fill_rounded(inner_r, bg_color, fg_color, 2);
|
||||
rect_fill_corners(inner_r, bg_color);
|
||||
}
|
||||
}
|
||||
|
||||
/// Filling all four corners of a rectangle with a given color.
|
||||
pub fn rect_fill_corners(r: Rect, fg_color: Color) {
|
||||
for p in r.corner_points().iter() {
|
||||
paint_point(p, fg_color);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub struct TextOverlay<'a> {
|
||||
pub struct TextOverlay<T> {
|
||||
area: Rect,
|
||||
text: &'a str,
|
||||
text: T,
|
||||
font: Font,
|
||||
}
|
||||
|
||||
impl<'a> TextOverlay<'a> {
|
||||
pub fn new(text: &'a str, font: Font) -> Self {
|
||||
impl<T: AsRef<str>> TextOverlay<T> {
|
||||
pub fn new(text: T, font: Font) -> Self {
|
||||
let area = Rect::zero();
|
||||
Self { area, text, font }
|
||||
}
|
||||
|
||||
pub fn set_text(&mut self, text: T) {
|
||||
self.text = text;
|
||||
}
|
||||
|
||||
pub fn get_text(&self) -> &T {
|
||||
&self.text
|
||||
}
|
||||
|
||||
// baseline relative to the underlying render area
|
||||
pub fn place(&mut self, baseline: Point) {
|
||||
let text_width = self.font.text_width(self.text);
|
||||
let text_width = self.font.text_width(self.text.as_ref());
|
||||
let text_height = self.font.text_height();
|
||||
|
||||
let text_area_start = baseline + Offset::new(-(text_width / 2), -text_height);
|
||||
@ -226,7 +270,12 @@ impl<'a> TextOverlay<'a> {
|
||||
|
||||
let p_rel = Point::new(p.x - self.area.x0, p.y - self.area.y0);
|
||||
|
||||
for g in self.text.bytes().filter_map(|c| self.font.get_glyph(c)) {
|
||||
for g in self
|
||||
.text
|
||||
.as_ref()
|
||||
.bytes()
|
||||
.filter_map(|c| self.font.get_glyph(c))
|
||||
{
|
||||
let char_area = Rect::new(
|
||||
Point::new(tot_adv + g.bearing_x, g.height - g.bearing_y),
|
||||
Point::new(tot_adv + g.bearing_x + g.width, g.bearing_y),
|
||||
@ -788,9 +837,9 @@ fn rect_rounded2_get_pixel(
|
||||
/// Optionally draws a text inside the rectangle and adjusts its color to match
|
||||
/// the fill. The coordinates of the text are specified in the TextOverlay
|
||||
/// struct.
|
||||
pub fn bar_with_text_and_fill(
|
||||
pub fn bar_with_text_and_fill<T: AsRef<str>>(
|
||||
area: Rect,
|
||||
overlay: Option<TextOverlay>,
|
||||
overlay: Option<&TextOverlay<T>>,
|
||||
fg_color: Color,
|
||||
bg_color: Color,
|
||||
fill_from: i16,
|
||||
@ -833,6 +882,19 @@ pub fn dotted_line(start: Point, width: i16, color: Color) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws a horizontal line of pixels with a given step of pixels.
|
||||
/// Giving `step=1` draws a full line.
|
||||
pub fn dotted_line_horizontal(start: Point, width: i16, color: Color, step: usize) {
|
||||
for x in (start.x..width).step_by(step) {
|
||||
paint_point(&Point::new(x, start.y), color);
|
||||
}
|
||||
}
|
||||
|
||||
/// Paints a pixel with a specific color on a given point.
|
||||
pub fn paint_point(point: &Point, color: Color) {
|
||||
display::bar(point.x, point.y, 1, 1, color.into());
|
||||
}
|
||||
|
||||
pub fn qrcode(center: Point, data: &str, max_size: u32, case_sensitive: bool) -> Result<(), Error> {
|
||||
qr::render_qrcode(center.x, center.y, data, max_size, case_sensitive)
|
||||
}
|
||||
@ -904,244 +966,3 @@ pub fn get_color_table(fg_color: Color, bg_color: Color) -> [Color; 16] {
|
||||
|
||||
table
|
||||
}
|
||||
|
||||
pub struct Glyph {
|
||||
pub width: i16,
|
||||
pub height: i16,
|
||||
pub adv: i16,
|
||||
pub bearing_x: i16,
|
||||
pub bearing_y: i16,
|
||||
data: &'static [u8],
|
||||
}
|
||||
|
||||
impl Glyph {
|
||||
/// Construct a `Glyph` from a raw pointer.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// This function is unsafe because the caller has to guarantee that `data`
|
||||
/// is pointing to a memory containing a valid glyph data, that is:
|
||||
/// - contains valid glyph metadata
|
||||
/// - data has appropriate size
|
||||
/// - data must have static lifetime
|
||||
pub unsafe fn load(data: *const u8) -> Self {
|
||||
unsafe {
|
||||
let width = *data.offset(0) as i16;
|
||||
let height = *data.offset(1) as i16;
|
||||
|
||||
let data_bits = constant::FONT_BPP * width * height;
|
||||
|
||||
let data_bytes = if data_bits % 8 == 0 {
|
||||
data_bits / 8
|
||||
} else {
|
||||
(data_bits / 8) + 1
|
||||
};
|
||||
|
||||
Glyph {
|
||||
width,
|
||||
height,
|
||||
adv: *data.offset(2) as i16,
|
||||
bearing_x: *data.offset(3) as i16,
|
||||
bearing_y: *data.offset(4) as i16,
|
||||
data: slice::from_raw_parts(data.offset(5), data_bytes as usize),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print(&self, pos: Point, colortable: [Color; 16]) -> i16 {
|
||||
let bearing = Offset::new(self.bearing_x, -self.bearing_y);
|
||||
let size = Offset::new(self.width, self.height);
|
||||
let pos_adj = pos + bearing;
|
||||
let r = Rect::from_top_left_and_size(pos_adj, size);
|
||||
|
||||
let area = r.translate(get_offset());
|
||||
let window = area.clamp(constant::screen());
|
||||
|
||||
set_window(window);
|
||||
|
||||
for y in window.y0..window.y1 {
|
||||
for x in window.x0..window.x1 {
|
||||
let p = Point::new(x, y);
|
||||
let r = p - pos_adj;
|
||||
let c = self.get_pixel_data(r);
|
||||
pixeldata(colortable[c as usize]);
|
||||
}
|
||||
}
|
||||
self.adv
|
||||
}
|
||||
|
||||
pub fn unpack_bpp1(&self, a: i16) -> u8 {
|
||||
let c_data = self.data[(a / 8) as usize];
|
||||
((c_data >> (7 - (a % 8))) & 0x01) * 15
|
||||
}
|
||||
|
||||
pub fn unpack_bpp2(&self, a: i16) -> u8 {
|
||||
let c_data = self.data[(a / 4) as usize];
|
||||
((c_data >> (6 - (a % 4) * 2)) & 0x03) * 5
|
||||
}
|
||||
|
||||
pub fn unpack_bpp4(&self, a: i16) -> u8 {
|
||||
let c_data = self.data[(a / 2) as usize];
|
||||
(c_data >> (4 - (a % 2) * 4)) & 0x0F
|
||||
}
|
||||
|
||||
pub fn unpack_bpp8(&self, a: i16) -> u8 {
|
||||
let c_data = self.data[a as usize];
|
||||
c_data >> 4
|
||||
}
|
||||
|
||||
pub fn get_pixel_data(&self, p: Offset) -> u8 {
|
||||
let a = p.x + p.y * self.width;
|
||||
|
||||
match constant::FONT_BPP {
|
||||
1 => self.unpack_bpp1(a),
|
||||
2 => self.unpack_bpp2(a),
|
||||
4 => self.unpack_bpp4(a),
|
||||
8 => self.unpack_bpp8(a),
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Font constants. Keep in sync with FONT_ definitions in
|
||||
/// `extmod/modtrezorui/fonts/fonts.h`.
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum Font {
|
||||
NORMAL = 1,
|
||||
BOLD = 2,
|
||||
MONO = 3,
|
||||
DEMIBOLD = 5,
|
||||
}
|
||||
|
||||
impl From<Font> for i32 {
|
||||
fn from(font: Font) -> i32 {
|
||||
-(font as i32)
|
||||
}
|
||||
}
|
||||
|
||||
impl Font {
|
||||
pub fn text_width(self, text: &str) -> i16 {
|
||||
display::text_width(text, self.into()) as i16
|
||||
}
|
||||
|
||||
pub fn char_width(self, ch: char) -> i16 {
|
||||
display::char_width(ch, self.into()) as i16
|
||||
}
|
||||
|
||||
pub fn text_height(self) -> i16 {
|
||||
display::text_height(self.into()) as i16
|
||||
}
|
||||
|
||||
pub fn line_height(self) -> i16 {
|
||||
constant::LINE_SPACE + self.text_height()
|
||||
}
|
||||
|
||||
pub fn get_glyph(self, char_byte: u8) -> Option<Glyph> {
|
||||
let gl_data = display::get_char_glyph(char_byte, self.into());
|
||||
|
||||
if gl_data.is_null() {
|
||||
return None;
|
||||
}
|
||||
unsafe { Some(Glyph::load(gl_data)) }
|
||||
}
|
||||
|
||||
pub fn display_text(self, text: &str, baseline: Point, fg_color: Color, bg_color: Color) {
|
||||
let colortable = get_color_table(fg_color, bg_color);
|
||||
let mut adv_total = 0;
|
||||
for c in text.bytes() {
|
||||
let g = self.get_glyph(c);
|
||||
if let Some(gly) = g {
|
||||
let adv = gly.print(baseline + Offset::new(adv_total, 0), colortable);
|
||||
adv_total += adv;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the length of the longest suffix from a given `text`
|
||||
/// that will fit into the area `width` pixels wide.
|
||||
pub fn longest_suffix(self, width: i16, text: &str) -> usize {
|
||||
let mut text_width = 0;
|
||||
for (chars_from_right, c) in text.chars().rev().enumerate() {
|
||||
let c_width = self.char_width(c);
|
||||
if text_width + c_width > width {
|
||||
// Another character cannot be fitted, we're done.
|
||||
return chars_from_right;
|
||||
}
|
||||
text_width += c_width;
|
||||
}
|
||||
|
||||
text.len() // it fits in its entirety
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub struct Color(u16);
|
||||
|
||||
impl Color {
|
||||
pub const fn from_u16(val: u16) -> Self {
|
||||
Self(val)
|
||||
}
|
||||
|
||||
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
|
||||
let r = (r as u16 & 0xF8) << 8;
|
||||
let g = (g as u16 & 0xFC) << 3;
|
||||
let b = (b as u16 & 0xF8) >> 3;
|
||||
Self(r | g | b)
|
||||
}
|
||||
|
||||
pub const fn luminance(self) -> u32 {
|
||||
((self.r() as u32 * 299) / 1000)
|
||||
+ (self.g() as u32 * 587) / 1000
|
||||
+ (self.b() as u32 * 114) / 1000
|
||||
}
|
||||
|
||||
pub const fn r(self) -> u8 {
|
||||
(self.0 >> 8) as u8 & 0xF8
|
||||
}
|
||||
|
||||
pub const fn g(self) -> u8 {
|
||||
(self.0 >> 3) as u8 & 0xFC
|
||||
}
|
||||
|
||||
pub const fn b(self) -> u8 {
|
||||
(self.0 << 3) as u8 & 0xF8
|
||||
}
|
||||
|
||||
pub fn to_u16(self) -> u16 {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn hi_byte(self) -> u8 {
|
||||
(self.to_u16() >> 8) as u8
|
||||
}
|
||||
|
||||
pub fn lo_byte(self) -> u8 {
|
||||
(self.to_u16() & 0xFF) as u8
|
||||
}
|
||||
|
||||
pub fn negate(self) -> Self {
|
||||
Self(!self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Lerp for Color {
|
||||
fn lerp(a: Self, b: Self, t: f32) -> Self {
|
||||
let r = u8::lerp(a.r(), b.r(), t);
|
||||
let g = u8::lerp(a.g(), b.g(), t);
|
||||
let b = u8::lerp(a.b(), b.b(), t);
|
||||
Color::rgb(r, g, b)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u16> for Color {
|
||||
fn from(val: u16) -> Self {
|
||||
Self(val)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for u16 {
|
||||
fn from(val: Color) -> Self {
|
||||
val.to_u16()
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
Self {
|
||||
x0: p.x - size.x / 2,
|
||||
@ -294,10 +309,20 @@ impl Rect {
|
||||
self.bottom_left().center(self.bottom_right())
|
||||
}
|
||||
|
||||
pub const fn left_center(&self) -> Point {
|
||||
self.bottom_left().center(self.top_left())
|
||||
}
|
||||
|
||||
pub const fn right_center(&self) -> Point {
|
||||
self.bottom_right().center(self.top_right())
|
||||
}
|
||||
|
||||
/// Whether a `Point` is inside the `Rect`.
|
||||
pub const fn contains(&self, point: Point) -> bool {
|
||||
point.x >= self.x0 && point.x < self.x1 && point.y >= self.y0 && point.y < self.y1
|
||||
}
|
||||
|
||||
/// Create a bigger `Rect` that contains both `self` and `other`.
|
||||
pub const fn union(&self, other: Self) -> Self {
|
||||
Self {
|
||||
x0: min(self.x0, other.x0),
|
||||
@ -307,6 +332,8 @@ impl Rect {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a smaller `Rect` from the bigger one by moving
|
||||
/// all the four sides closer to the center.
|
||||
pub const fn inset(&self, insets: Insets) -> Self {
|
||||
Self {
|
||||
x0: self.x0 + insets.left,
|
||||
@ -316,6 +343,12 @@ impl Rect {
|
||||
}
|
||||
}
|
||||
|
||||
/// Move all the sides closer to the center by the same distance.
|
||||
pub const fn shrink(&self, size: i16) -> Self {
|
||||
self.inset(Insets::uniform(size))
|
||||
}
|
||||
|
||||
/// Leave just the left side of a certain `width`.
|
||||
pub const fn cut_from_left(&self, width: i16) -> Self {
|
||||
Self {
|
||||
x0: self.x0,
|
||||
@ -325,6 +358,7 @@ impl Rect {
|
||||
}
|
||||
}
|
||||
|
||||
/// Leave just the right side of a certain `width`.
|
||||
pub const fn cut_from_right(&self, width: i16) -> Self {
|
||||
Self {
|
||||
x0: self.x1 - width,
|
||||
@ -334,6 +368,27 @@ impl Rect {
|
||||
}
|
||||
}
|
||||
|
||||
/// Make the `Rect` wider to the left side.
|
||||
pub const fn extend_left(&self, width: i16) -> Self {
|
||||
Self {
|
||||
x0: self.x0 - width,
|
||||
y0: self.y0,
|
||||
x1: self.x1,
|
||||
y1: self.y1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Make the `Rect` wider to the right side.
|
||||
pub const fn extend_right(&self, width: i16) -> Self {
|
||||
Self {
|
||||
x0: self.x0,
|
||||
y0: self.y0,
|
||||
x1: self.x1 + width,
|
||||
y1: self.y1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Split `Rect` into top and bottom, given the top one's `height`.
|
||||
pub const fn split_top(self, height: i16) -> (Self, Self) {
|
||||
let height = clamp(height, 0, self.height());
|
||||
|
||||
@ -348,10 +403,12 @@ impl Rect {
|
||||
(top, bottom)
|
||||
}
|
||||
|
||||
/// Split `Rect` into top and bottom, given the bottom one's `height`.
|
||||
pub const fn split_bottom(self, height: i16) -> (Self, Self) {
|
||||
self.split_top(self.height() - height)
|
||||
}
|
||||
|
||||
/// Split `Rect` into left and right, given the left one's `width`.
|
||||
pub const fn split_left(self, width: i16) -> (Self, Self) {
|
||||
let width = clamp(width, 0, self.width());
|
||||
|
||||
@ -366,10 +423,21 @@ impl Rect {
|
||||
(left, right)
|
||||
}
|
||||
|
||||
/// Split `Rect` into left and right, given the right one's `width`.
|
||||
pub const fn split_right(self, width: i16) -> (Self, Self) {
|
||||
self.split_left(self.width() - width)
|
||||
}
|
||||
|
||||
/// Split `Rect` into left, center and right, given the center one's
|
||||
/// `width`. Center element is symmetric, left and right have the same
|
||||
/// size.
|
||||
pub const fn split_center(self, width: i16) -> (Self, Self, Self) {
|
||||
let left_right_width = (self.width() - width) / 2;
|
||||
let (left, center_right) = self.split_left(left_right_width);
|
||||
let (center, right) = center_right.split_left(width);
|
||||
(left, center, right)
|
||||
}
|
||||
|
||||
pub const fn clamp(self, limit: Rect) -> Self {
|
||||
Self {
|
||||
x0: max(self.x0, limit.x0),
|
||||
@ -387,6 +455,7 @@ impl Rect {
|
||||
}
|
||||
}
|
||||
|
||||
/// Moving `Rect` by the given offset.
|
||||
pub const fn translate(&self, offset: Offset) -> Self {
|
||||
Self {
|
||||
x0: self.x0 + offset.x,
|
||||
@ -395,6 +464,16 @@ impl Rect {
|
||||
y1: self.y1 + offset.y,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all four corner points.
|
||||
pub fn corner_points(&self) -> [Point; 4] {
|
||||
[
|
||||
self.top_left(),
|
||||
self.top_right() - Offset::x(1),
|
||||
self.bottom_right() - Offset::uniform(1),
|
||||
self.bottom_left() - Offset::y(1),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
@ -509,7 +588,7 @@ impl Grid {
|
||||
let cell_height = (self.area.height() - spacing_height) / nrows;
|
||||
|
||||
// Not every area can be fully covered by equal-sized cells and spaces, there
|
||||
// might be serveral pixels left unused. We'll distribute them by 1px to
|
||||
// might be several pixels left unused. We'll distribute them by 1px to
|
||||
// the leftmost cells.
|
||||
let leftover_width = (self.area.width() - spacing_width) % ncols;
|
||||
let leftover_height = (self.area.height() - spacing_height) % nrows;
|
||||
|
@ -196,6 +196,14 @@ impl LayoutObj {
|
||||
unsafe { Gc::as_mut(&mut inner.root) }.obj_paint();
|
||||
}
|
||||
|
||||
/// Place but do not paint.
|
||||
/// Called before getting debug information about current screen.
|
||||
fn obj_place(&self) {
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
// SAFETY: `inner.root` is unique because of the `inner.borrow_mut()`.
|
||||
unsafe { Gc::as_mut(&mut inner.root) }.obj_place(constant::screen());
|
||||
}
|
||||
|
||||
/// Run a tracing pass over the component tree. Passed `callback` is called
|
||||
/// with each piece of tracing information. Panics in case the callback
|
||||
/// raises an exception.
|
||||
@ -219,30 +227,51 @@ impl LayoutObj {
|
||||
}
|
||||
|
||||
fn symbol(&mut self, name: &str) {
|
||||
self.0
|
||||
.call_with_n_args(&[
|
||||
"<".try_into().unwrap(),
|
||||
name.try_into().unwrap(),
|
||||
">".try_into().unwrap(),
|
||||
])
|
||||
.unwrap();
|
||||
self.string("<");
|
||||
self.string(name);
|
||||
self.string(">");
|
||||
}
|
||||
|
||||
fn open(&mut self, name: &str) {
|
||||
self.0
|
||||
.call_with_n_args(&["<".try_into().unwrap(), name.try_into().unwrap()])
|
||||
.unwrap();
|
||||
self.string("<");
|
||||
self.string(name);
|
||||
self.string(" ");
|
||||
}
|
||||
|
||||
fn field(&mut self, name: &str, value: &dyn Trace) {
|
||||
self.0
|
||||
.call_with_n_args(&[name.try_into().unwrap(), ": ".try_into().unwrap()])
|
||||
.unwrap();
|
||||
self.string(name);
|
||||
self.string(":");
|
||||
value.trace(self);
|
||||
self.string(" ");
|
||||
}
|
||||
|
||||
/// Mark the string as a title/header.
|
||||
fn title(&mut self, title: &str) {
|
||||
self.string(crate::trace::TITLE_TAG);
|
||||
self.string(title);
|
||||
self.string(crate::trace::TITLE_TAG);
|
||||
}
|
||||
|
||||
/// Mark the string as a button content.
|
||||
fn button(&mut self, button: &str) {
|
||||
self.string(crate::trace::BTN_TAG);
|
||||
self.string(button);
|
||||
self.string(crate::trace::BTN_TAG);
|
||||
}
|
||||
|
||||
fn content_flag(&mut self) {
|
||||
self.string(crate::trace::CONTENT_TAG);
|
||||
}
|
||||
|
||||
fn kw_pair(&mut self, key: &str, value: &str) {
|
||||
self.string(key);
|
||||
self.string("::");
|
||||
self.string(value);
|
||||
self.string(","); // mostly for human readability
|
||||
}
|
||||
|
||||
fn close(&mut self) {
|
||||
self.0.call_with_n_args(&[">".try_into().unwrap()]).unwrap();
|
||||
self.string(">")
|
||||
}
|
||||
}
|
||||
|
||||
@ -283,6 +312,7 @@ impl LayoutObj {
|
||||
Qstr::MP_QSTR_timer => obj_fn_2!(ui_layout_timer).as_obj(),
|
||||
Qstr::MP_QSTR_paint => obj_fn_1!(ui_layout_paint).as_obj(),
|
||||
Qstr::MP_QSTR_request_complete_repaint => obj_fn_1!(ui_layout_request_complete_repaint).as_obj(),
|
||||
Qstr::MP_QSTR_place => obj_fn_1!(ui_layout_place).as_obj(),
|
||||
Qstr::MP_QSTR_trace => obj_fn_2!(ui_layout_trace).as_obj(),
|
||||
Qstr::MP_QSTR_bounds => obj_fn_1!(ui_layout_bounds).as_obj(),
|
||||
Qstr::MP_QSTR_page_count => obj_fn_1!(ui_layout_page_count).as_obj(),
|
||||
@ -438,6 +468,15 @@ extern "C" fn ui_layout_request_complete_repaint(this: Obj) -> Obj {
|
||||
unsafe { util::try_or_raise(block) }
|
||||
}
|
||||
|
||||
extern "C" fn ui_layout_place(this: Obj) -> Obj {
|
||||
let block = || {
|
||||
let this: Gc<LayoutObj> = this.try_into()?;
|
||||
this.obj_place();
|
||||
Ok(Obj::const_true())
|
||||
};
|
||||
unsafe { util::try_or_raise(block) }
|
||||
}
|
||||
|
||||
extern "C" fn ui_layout_page_count(this: Obj) -> Obj {
|
||||
let block = || {
|
||||
let this: Gc<LayoutObj> = this.try_into()?;
|
||||
|
@ -20,6 +20,16 @@ pub fn iter_into_objs<const N: usize>(iterable: Obj) -> Result<[Obj; N], Error>
|
||||
}
|
||||
|
||||
pub fn iter_into_array<T, const N: usize>(iterable: Obj) -> Result<[T; N], Error>
|
||||
where
|
||||
T: TryFrom<Obj, Error = Error>,
|
||||
{
|
||||
let err = Error::ValueError(cstr!("Invalid iterable length"));
|
||||
let vec: Vec<T, N> = iter_into_vec(iterable)?;
|
||||
// Returns error if array.len() != N
|
||||
vec.into_array().map_err(|_| err)
|
||||
}
|
||||
|
||||
pub fn iter_into_vec<T, const N: usize>(iterable: Obj) -> Result<Vec<T, N>, Error>
|
||||
where
|
||||
T: TryFrom<Obj, Error = Error>,
|
||||
{
|
||||
@ -29,6 +39,5 @@ where
|
||||
for item in Iter::try_from_obj_with_buf(iterable, &mut iter_buf)? {
|
||||
vec.push(item.try_into()?).map_err(|_| err)?;
|
||||
}
|
||||
// Returns error if array.len() != N
|
||||
vec.into_array().map_err(|_| err)
|
||||
Ok(vec)
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -57,10 +57,10 @@ extern "C" fn new_confirm_action(n_args: usize, args: *const Obj, kwargs: *mut M
|
||||
let reverse: bool = kwargs.get(Qstr::MP_QSTR_reverse)?.try_into()?;
|
||||
|
||||
let format = match (&action, &description, reverse) {
|
||||
(Some(_), Some(_), false) => "{bold}{action}\n\r{normal}{description}",
|
||||
(Some(_), Some(_), true) => "{normal}{description}\n\r{bold}{action}",
|
||||
(Some(_), None, _) => "{bold}{action}",
|
||||
(None, Some(_), _) => "{normal}{description}",
|
||||
(Some(_), Some(_), false) => "{Font::bold}{action}\n\r{Font::normal}{description}",
|
||||
(Some(_), Some(_), true) => "{Font::normal}{description}\n\r{Font::bold}{action}",
|
||||
(Some(_), None, _) => "{Font::bold}{action}",
|
||||
(None, Some(_), _) => "{Font::normal}{description}",
|
||||
_ => "",
|
||||
};
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
282
core/embed/rust/src/ui/model_tr/component/bip39.rs
Normal file
@ -0,0 +1,282 @@
|
||||
use crate::{
|
||||
trezorhal::bip39,
|
||||
ui::{
|
||||
component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx},
|
||||
geometry::Rect,
|
||||
},
|
||||
};
|
||||
|
||||
use super::{
|
||||
choice_item::BigCharacterChoiceItem, ButtonDetails, ButtonLayout, ChangingTextLine,
|
||||
ChoiceFactory, ChoiceItem, ChoicePage, ChoicePageMsg, TextChoiceItem,
|
||||
};
|
||||
use heapless::{String, Vec};
|
||||
|
||||
pub enum Bip39EntryMsg {
|
||||
ResultWord(String<15>),
|
||||
}
|
||||
|
||||
const CURRENT_LETTERS_ROW: i32 = 25;
|
||||
|
||||
const MAX_LENGTH: usize = 10;
|
||||
const MAX_CHOICE_LENGTH: usize = 26;
|
||||
|
||||
/// Offer words when there will be fewer of them than this
|
||||
const OFFER_WORDS_THRESHOLD: usize = 10;
|
||||
|
||||
struct ChoiceFactoryBIP39 {
|
||||
// TODO: replace these Vecs by iterators somehow?
|
||||
letter_choices: Option<Vec<char, MAX_CHOICE_LENGTH>>,
|
||||
word_choices: Option<Vec<&'static str, OFFER_WORDS_THRESHOLD>>,
|
||||
}
|
||||
impl ChoiceFactoryBIP39 {
|
||||
fn new(
|
||||
letter_choices: Option<Vec<char, MAX_CHOICE_LENGTH>>,
|
||||
word_choices: Option<Vec<&'static str, OFFER_WORDS_THRESHOLD>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
letter_choices,
|
||||
word_choices,
|
||||
}
|
||||
}
|
||||
|
||||
fn letters(letter_choices: Vec<char, MAX_CHOICE_LENGTH>) -> Self {
|
||||
Self::new(Some(letter_choices), None)
|
||||
}
|
||||
|
||||
fn words(word_choices: Vec<&'static str, OFFER_WORDS_THRESHOLD>) -> Self {
|
||||
Self::new(None, Some(word_choices))
|
||||
}
|
||||
|
||||
/// Word choice items with BIN leftmost button.
|
||||
fn get_word_item(&self, choice_index: u8) -> ChoiceItem {
|
||||
if let Some(word_choices) = &self.word_choices {
|
||||
let word = word_choices[choice_index as usize];
|
||||
let choice = TextChoiceItem::new(word, ButtonLayout::default_three_icons());
|
||||
let mut word_item = ChoiceItem::Text(choice);
|
||||
|
||||
// Adding BIN leftmost button and removing the rightmost one.
|
||||
if choice_index == 0 {
|
||||
word_item.set_left_btn(Some(ButtonDetails::bin_icon()));
|
||||
} else if choice_index as usize == word_choices.len() - 1 {
|
||||
word_item.set_right_btn(None);
|
||||
}
|
||||
|
||||
word_item
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
/// Letter choice items with BIN leftmost button. Letters are BIG.
|
||||
fn get_letter_item(&self, choice_index: u8) -> ChoiceItem {
|
||||
// TODO: we could support carousel for letters to quicken it for users
|
||||
// (but then the BIN would need to be an option on its own, not so
|
||||
// user-friendly)
|
||||
if let Some(letter_choices) = &self.letter_choices {
|
||||
let letter = letter_choices[choice_index as usize];
|
||||
let letter_choice =
|
||||
BigCharacterChoiceItem::new(letter, ButtonLayout::default_three_icons());
|
||||
let mut letter_item = ChoiceItem::BigCharacter(letter_choice);
|
||||
|
||||
// Adding BIN leftmost button and removing the rightmost one.
|
||||
if choice_index == 0 {
|
||||
letter_item.set_left_btn(Some(ButtonDetails::bin_icon()));
|
||||
} else if choice_index as usize == letter_choices.len() - 1 {
|
||||
letter_item.set_right_btn(None);
|
||||
}
|
||||
|
||||
letter_item
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
impl ChoiceFactory for ChoiceFactoryBIP39 {
|
||||
fn get(&self, choice_index: u8) -> ChoiceItem {
|
||||
if self.letter_choices.is_some() {
|
||||
self.get_letter_item(choice_index)
|
||||
} else if self.word_choices.is_some() {
|
||||
self.get_word_item(choice_index)
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
fn count(&self) -> u8 {
|
||||
if let Some(letter_choices) = &self.letter_choices {
|
||||
letter_choices.len() as u8
|
||||
} else if let Some(word_choices) = &self.word_choices {
|
||||
word_choices.len() as u8
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Component for entering a BIP39 mnemonic.
|
||||
pub struct Bip39Entry {
|
||||
choice_page: ChoicePage<ChoiceFactoryBIP39>,
|
||||
chosen_letters: Child<ChangingTextLine<String<{ MAX_LENGTH + 1 }>>>,
|
||||
letter_choices: Vec<char, MAX_CHOICE_LENGTH>,
|
||||
textbox: TextBox<MAX_LENGTH>,
|
||||
offer_words: bool,
|
||||
words_list: bip39::Wordlist,
|
||||
}
|
||||
|
||||
impl Bip39Entry {
|
||||
pub fn new() -> Self {
|
||||
let letter_choices: Vec<char, MAX_CHOICE_LENGTH> =
|
||||
bip39::get_available_letters("").collect();
|
||||
let choices = ChoiceFactoryBIP39::letters(letter_choices.clone());
|
||||
|
||||
Self {
|
||||
choice_page: ChoicePage::new(choices),
|
||||
chosen_letters: Child::new(ChangingTextLine::center_mono(String::new())),
|
||||
letter_choices,
|
||||
textbox: TextBox::empty(),
|
||||
offer_words: false,
|
||||
words_list: bip39::Wordlist::all(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets up-to-date choices for letters or words.
|
||||
fn get_current_choices(&mut self) -> ChoiceFactoryBIP39 {
|
||||
// Narrowing the word list
|
||||
self.words_list = self.words_list.filter_prefix(self.textbox.content());
|
||||
|
||||
// Offering words when there is only a few of them
|
||||
// Otherwise getting relevant letters
|
||||
if self.words_list.len() < OFFER_WORDS_THRESHOLD {
|
||||
self.offer_words = true;
|
||||
let word_choices = self.words_list.iter().collect();
|
||||
ChoiceFactoryBIP39::words(word_choices)
|
||||
} else {
|
||||
self.offer_words = false;
|
||||
self.letter_choices = bip39::get_available_letters(self.textbox.content()).collect();
|
||||
ChoiceFactoryBIP39::letters(self.letter_choices.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn update_chosen_letters(&mut self, ctx: &mut EventCtx) {
|
||||
let text = build_string!({ MAX_LENGTH + 1 }, self.textbox.content(), "_");
|
||||
self.chosen_letters.inner_mut().update_text(text);
|
||||
self.chosen_letters.request_complete_repaint(ctx);
|
||||
}
|
||||
|
||||
fn append_letter(&mut self, ctx: &mut EventCtx, letter: char) {
|
||||
self.textbox.append(ctx, letter);
|
||||
}
|
||||
|
||||
fn delete_last_letter(&mut self, ctx: &mut EventCtx) {
|
||||
self.textbox.delete_last(ctx);
|
||||
}
|
||||
|
||||
fn reset_wordlist(&mut self) {
|
||||
self.words_list = bip39::Wordlist::all();
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Bip39Entry {
|
||||
type Msg = Bip39EntryMsg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
let letters_area_height = self.chosen_letters.inner().needed_height();
|
||||
let (letters_area, choice_area) = bounds.split_top(letters_area_height);
|
||||
self.chosen_letters.place(letters_area);
|
||||
self.choice_page.place(choice_area);
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
let msg = self.choice_page.event(ctx, event);
|
||||
match msg {
|
||||
Some(ChoicePageMsg::Choice(page_counter)) => {
|
||||
// Clicked SELECT.
|
||||
// When we already offer words, return the word at the given index.
|
||||
// Otherwise, appending the new letter and resetting the choice page
|
||||
// with up-to-date choices.
|
||||
if self.offer_words {
|
||||
let word = self
|
||||
.words_list
|
||||
.get(page_counter as usize)
|
||||
.unwrap_or_default();
|
||||
return Some(Bip39EntryMsg::ResultWord(String::from(word)));
|
||||
} else {
|
||||
let new_letter = self.letter_choices[page_counter as usize];
|
||||
self.append_letter(ctx, new_letter);
|
||||
self.update_chosen_letters(ctx);
|
||||
let new_choices = self.get_current_choices();
|
||||
self.choice_page.reset(ctx, new_choices, true, false);
|
||||
ctx.request_paint();
|
||||
}
|
||||
}
|
||||
Some(ChoicePageMsg::LeftMost) => {
|
||||
// Clicked BIN. Deleting last letter, updating wordlist and updating choices
|
||||
self.delete_last_letter(ctx);
|
||||
self.update_chosen_letters(ctx);
|
||||
self.reset_wordlist();
|
||||
let new_choices = self.get_current_choices();
|
||||
self.choice_page.reset(ctx, new_choices, true, false);
|
||||
ctx.request_paint();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.chosen_letters.paint();
|
||||
self.choice_page.paint();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
use super::{ButtonAction, ButtonPos};
|
||||
#[cfg(feature = "ui_debug")]
|
||||
use crate::ui::util;
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for Bip39Entry {
|
||||
fn get_btn_action(&self, pos: ButtonPos) -> String<25> {
|
||||
match pos {
|
||||
ButtonPos::Left => match self.choice_page.has_previous_choice() {
|
||||
true => ButtonAction::PrevPage.string(),
|
||||
false => ButtonAction::Action("Delete last char").string(),
|
||||
},
|
||||
ButtonPos::Right => match self.choice_page.has_next_choice() {
|
||||
true => ButtonAction::NextPage.string(),
|
||||
false => ButtonAction::empty(),
|
||||
},
|
||||
ButtonPos::Middle => {
|
||||
let current_index = self.choice_page.page_index() as usize;
|
||||
let choice: String<10> = if self.offer_words {
|
||||
self.words_list
|
||||
.get(current_index)
|
||||
.unwrap_or_default()
|
||||
.into()
|
||||
} else {
|
||||
util::char_to_string(self.letter_choices[current_index])
|
||||
};
|
||||
ButtonAction::select_item(choice)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("Bip39Entry");
|
||||
t.kw_pair("textbox", self.textbox.content());
|
||||
|
||||
self.report_btn_actions(t);
|
||||
|
||||
t.open("letter_choices");
|
||||
for ch in &self.letter_choices {
|
||||
t.string(&util::char_to_string::<1>(*ch));
|
||||
}
|
||||
t.close();
|
||||
|
||||
t.field("choice_page", &self.choice_page);
|
||||
t.close();
|
||||
}
|
||||
}
|
@ -1,19 +1,30 @@
|
||||
use crate::ui::{
|
||||
component::{Component, Event, EventCtx},
|
||||
display::{self, Color, Font},
|
||||
event::{ButtonEvent, PhysicalButton},
|
||||
geometry::{Offset, Point, Rect},
|
||||
use crate::{
|
||||
time::Duration,
|
||||
ui::{
|
||||
component::{Component, Event, EventCtx},
|
||||
constant,
|
||||
display::{self, Color, Font, Icon},
|
||||
event::{ButtonEvent, PhysicalButton},
|
||||
geometry::{Offset, Point, Rect},
|
||||
},
|
||||
};
|
||||
|
||||
use heapless::String;
|
||||
|
||||
use super::theme;
|
||||
|
||||
const HALF_SCREEN_BUTTON_WIDTH: i16 = constant::WIDTH / 2 - 1;
|
||||
|
||||
#[derive(Eq, PartialEq)]
|
||||
pub enum ButtonMsg {
|
||||
Clicked,
|
||||
LongPressed,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum ButtonPos {
|
||||
Left,
|
||||
Middle,
|
||||
Right,
|
||||
}
|
||||
|
||||
@ -21,15 +32,16 @@ impl ButtonPos {
|
||||
pub fn hit(&self, b: &PhysicalButton) -> bool {
|
||||
matches!(
|
||||
(self, b),
|
||||
(Self::Left, PhysicalButton::Left) | (Self::Right, PhysicalButton::Right)
|
||||
(Self::Left, PhysicalButton::Left)
|
||||
| (Self::Middle, PhysicalButton::Both)
|
||||
| (Self::Right, PhysicalButton::Right)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Button<T> {
|
||||
area: Rect,
|
||||
bounds: Rect,
|
||||
pos: ButtonPos,
|
||||
baseline: Point,
|
||||
content: ButtonContent<T>,
|
||||
styles: ButtonStyleSheet,
|
||||
state: State,
|
||||
@ -41,8 +53,7 @@ impl<T: AsRef<str>> Button<T> {
|
||||
pos,
|
||||
content,
|
||||
styles,
|
||||
baseline: Point::zero(),
|
||||
area: Rect::zero(),
|
||||
bounds: Rect::zero(),
|
||||
state: State::Released,
|
||||
}
|
||||
}
|
||||
@ -51,7 +62,7 @@ impl<T: AsRef<str>> Button<T> {
|
||||
Self::new(pos, ButtonContent::Text(text), styles)
|
||||
}
|
||||
|
||||
pub fn with_icon(pos: ButtonPos, image: &'static [u8], styles: ButtonStyleSheet) -> Self {
|
||||
pub fn with_icon(pos: ButtonPos, image: Icon, styles: ButtonStyleSheet) -> Self {
|
||||
Self::new(pos, ButtonContent::Icon(image), styles)
|
||||
}
|
||||
|
||||
@ -59,13 +70,29 @@ impl<T: AsRef<str>> Button<T> {
|
||||
&self.content
|
||||
}
|
||||
|
||||
fn style(&self) -> &ButtonStyle {
|
||||
fn style(&self) -> ButtonStyle {
|
||||
match self.state {
|
||||
State::Released => self.styles.normal,
|
||||
State::Pressed => self.styles.active,
|
||||
}
|
||||
}
|
||||
|
||||
/// Changing the icon content of the button.
|
||||
pub fn set_icon(&mut self, image: Icon) {
|
||||
self.content = ButtonContent::Icon(image);
|
||||
}
|
||||
|
||||
/// Changing the text content of the button.
|
||||
pub fn set_text(&mut self, text: T) {
|
||||
self.content = ButtonContent::Text(text);
|
||||
}
|
||||
|
||||
/// Changing the style of the button.
|
||||
pub fn set_style(&mut self, styles: ButtonStyleSheet) {
|
||||
self.styles = styles;
|
||||
}
|
||||
|
||||
// Setting the visual state of the button.
|
||||
fn set(&mut self, ctx: &mut EventCtx, state: State) {
|
||||
if self.state != state {
|
||||
self.state = state;
|
||||
@ -73,26 +100,75 @@ impl<T: AsRef<str>> Button<T> {
|
||||
}
|
||||
}
|
||||
|
||||
fn placement(
|
||||
area: Rect,
|
||||
pos: ButtonPos,
|
||||
content: &ButtonContent<T>,
|
||||
styles: &ButtonStyleSheet,
|
||||
) -> (Rect, Point) {
|
||||
let border_width = if styles.normal.border_horiz { 2 } else { 0 };
|
||||
let content_width = match content {
|
||||
ButtonContent::Text(text) => styles.normal.font.text_width(text.as_ref()) - 1,
|
||||
ButtonContent::Icon(_icon) => todo!(),
|
||||
// Setting the visual state of the button.
|
||||
pub fn set_pressed(&mut self, ctx: &mut EventCtx, is_pressed: bool) {
|
||||
let new_state = if is_pressed {
|
||||
State::Pressed
|
||||
} else {
|
||||
State::Released
|
||||
};
|
||||
let button_width = content_width + 2 * border_width;
|
||||
let area = match pos {
|
||||
ButtonPos::Left => area.split_left(button_width).0,
|
||||
ButtonPos::Right => area.split_right(button_width).1,
|
||||
self.set(ctx, new_state);
|
||||
}
|
||||
|
||||
/// Return the full area of the button according
|
||||
/// to its current style, content and position.
|
||||
fn get_current_area(&self) -> Rect {
|
||||
let style = self.style();
|
||||
|
||||
// Button width may be forced. Otherwise calculate it.
|
||||
let button_width = if let Some(width) = style.force_width {
|
||||
width
|
||||
} else {
|
||||
let outline = if style.with_outline {
|
||||
theme::BUTTON_OUTLINE
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let content_width = match &self.content {
|
||||
ButtonContent::Text(text) => style.font.text_width(text.as_ref()) - 1,
|
||||
ButtonContent::Icon(icon) => icon.width() - 1,
|
||||
};
|
||||
content_width + 2 * outline
|
||||
};
|
||||
|
||||
let start_of_baseline = area.bottom_left() + Offset::new(border_width, -2);
|
||||
// Button height may be adjusted for the icon without outline
|
||||
// Done to avoid highlighting bigger area than necessary when
|
||||
// drawing the icon in active (black on white) state
|
||||
let button_height = match &self.content {
|
||||
ButtonContent::Text(_) => theme::BUTTON_HEIGHT,
|
||||
ButtonContent::Icon(icon) => {
|
||||
if style.with_outline {
|
||||
theme::BUTTON_HEIGHT
|
||||
} else {
|
||||
icon.height()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
(area, start_of_baseline)
|
||||
let button_bounds = self.bounds.split_bottom(button_height).1;
|
||||
let area = match self.pos {
|
||||
ButtonPos::Left => button_bounds.split_left(button_width).0,
|
||||
ButtonPos::Right => button_bounds.split_right(button_width).1,
|
||||
ButtonPos::Middle => button_bounds.split_center(button_width).1,
|
||||
};
|
||||
|
||||
// Allowing for possible offset of the area from current style
|
||||
if let Some(offset) = style.offset {
|
||||
area.translate(offset)
|
||||
} else {
|
||||
area
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine baseline point for the text.
|
||||
fn get_text_baseline(&self, style: &ButtonStyle) -> Point {
|
||||
// Arms and outline require the text to be elevated.
|
||||
if style.with_arms || style.with_outline {
|
||||
let offset = theme::BUTTON_OUTLINE;
|
||||
self.get_current_area().bottom_left() + Offset::new(offset, -offset)
|
||||
} else {
|
||||
self.get_current_area().bottom_left()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,13 +179,13 @@ where
|
||||
type Msg = ButtonMsg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
let (area, baseline) = Self::placement(bounds, self.pos, &self.content, &self.styles);
|
||||
self.area = area;
|
||||
self.baseline = baseline;
|
||||
self.area
|
||||
self.bounds = bounds;
|
||||
self.get_current_area()
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
// Everything should be handled by `ButtonController`
|
||||
// TODO: could be completely deleted, but `ResultPopup` is using Button.event()
|
||||
match event {
|
||||
Event::Button(ButtonEvent::ButtonPressed(which)) if self.pos.hit(&which) => {
|
||||
self.set(ctx, State::Pressed);
|
||||
@ -127,26 +203,74 @@ where
|
||||
|
||||
fn paint(&mut self) {
|
||||
let style = self.style();
|
||||
let text_color = style.text_color;
|
||||
let background_color = text_color.negate();
|
||||
let area = self.get_current_area();
|
||||
|
||||
// TODO: support another combinations of text and icons
|
||||
// - text with OK icon on left
|
||||
|
||||
// Optionally display "arms" at both sides of content, or create
|
||||
// a nice rounded outline around it.
|
||||
// By default just fill the content background.
|
||||
if style.with_arms {
|
||||
// Prepare space for both the arms and content with BG color.
|
||||
// Arms are icons 10*6 pixels.
|
||||
let area_to_fill = area.extend_left(10).extend_right(15);
|
||||
display::rect_fill(area_to_fill, background_color);
|
||||
|
||||
// Paint both arms.
|
||||
// Baselines need to be shifted little bit right to fit properly with the text
|
||||
// TODO: for "CONFIRM" there is one space at the right, but for "SELECT" there
|
||||
// are two
|
||||
Icon::new(theme::ICON_ARM_LEFT).draw_top_right(
|
||||
area.left_center() + Offset::x(2),
|
||||
text_color,
|
||||
background_color,
|
||||
);
|
||||
Icon::new(theme::ICON_ARM_RIGHT).draw_top_left(
|
||||
area.right_center() + Offset::x(4),
|
||||
text_color,
|
||||
background_color,
|
||||
);
|
||||
} else if style.with_outline {
|
||||
display::rect_outline_rounded(area, text_color, background_color, 2);
|
||||
} else {
|
||||
display::rect_fill(area, background_color)
|
||||
}
|
||||
|
||||
match &self.content {
|
||||
ButtonContent::Text(text) => {
|
||||
let background_color = style.text_color.negate();
|
||||
if style.border_horiz {
|
||||
display::rect_fill_rounded1(self.area, background_color, theme::BG);
|
||||
} else {
|
||||
display::rect_fill(self.area, background_color)
|
||||
}
|
||||
|
||||
display::text(
|
||||
self.baseline,
|
||||
self.get_text_baseline(&style),
|
||||
text.as_ref(),
|
||||
style.font,
|
||||
style.text_color,
|
||||
text_color,
|
||||
background_color,
|
||||
);
|
||||
}
|
||||
ButtonContent::Icon(_image) => {
|
||||
todo!();
|
||||
ButtonContent::Icon(icon) => {
|
||||
if style.with_outline {
|
||||
// Accounting for the 8*8 icon with empty left column and bottom row
|
||||
// (which fits the outline nicely and symmetrically)
|
||||
let center = area.center() + Offset::uniform(1);
|
||||
icon.draw_center(center, text_color, background_color);
|
||||
} else {
|
||||
// Positioning the icon in the corresponding corner/center
|
||||
match self.pos {
|
||||
ButtonPos::Left => {
|
||||
icon.draw_bottom_left(area.bottom_left(), text_color, background_color)
|
||||
}
|
||||
ButtonPos::Right => icon.draw_bottom_right(
|
||||
area.bottom_right(),
|
||||
text_color,
|
||||
background_color,
|
||||
),
|
||||
ButtonPos::Middle => {
|
||||
icon.draw_center(area.center(), text_color, background_color)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -161,7 +285,7 @@ where
|
||||
t.open("Button");
|
||||
match &self.content {
|
||||
ButtonContent::Text(text) => t.field("text", text),
|
||||
ButtonContent::Icon(_) => t.symbol("icon"),
|
||||
ButtonContent::Icon(icon) => t.field("icon", icon),
|
||||
}
|
||||
t.close();
|
||||
}
|
||||
@ -175,16 +299,486 @@ enum State {
|
||||
|
||||
pub enum ButtonContent<T> {
|
||||
Text(T),
|
||||
Icon(&'static [u8]),
|
||||
Icon(Icon),
|
||||
}
|
||||
|
||||
pub struct ButtonStyleSheet {
|
||||
pub normal: &'static ButtonStyle,
|
||||
pub active: &'static ButtonStyle,
|
||||
pub normal: ButtonStyle,
|
||||
pub active: ButtonStyle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ButtonStyle {
|
||||
pub font: Font,
|
||||
pub text_color: Color,
|
||||
pub border_horiz: bool,
|
||||
pub with_outline: bool,
|
||||
pub with_arms: bool,
|
||||
pub force_width: Option<i16>,
|
||||
pub offset: Option<Offset>,
|
||||
}
|
||||
|
||||
// TODO: currently `button_default` and `button_cancel`
|
||||
// are the same - decide whether to differentiate them.
|
||||
// In Figma, they are not differentiated.
|
||||
|
||||
impl ButtonStyleSheet {
|
||||
pub fn new(
|
||||
normal_color: Color,
|
||||
active_color: Color,
|
||||
with_outline: bool,
|
||||
with_arms: bool,
|
||||
force_width: Option<i16>,
|
||||
offset: Option<Offset>,
|
||||
) -> Self {
|
||||
Self {
|
||||
normal: ButtonStyle {
|
||||
font: theme::FONT_BUTTON,
|
||||
text_color: normal_color,
|
||||
with_outline,
|
||||
with_arms,
|
||||
force_width,
|
||||
offset,
|
||||
},
|
||||
active: ButtonStyle {
|
||||
font: theme::FONT_BUTTON,
|
||||
text_color: active_color,
|
||||
with_outline,
|
||||
with_arms,
|
||||
force_width,
|
||||
offset,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// White text in normal mode.
|
||||
pub fn default(
|
||||
with_outline: bool,
|
||||
with_arms: bool,
|
||||
force_width: Option<i16>,
|
||||
offset: Option<Offset>,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
theme::FG,
|
||||
theme::BG,
|
||||
with_outline,
|
||||
with_arms,
|
||||
force_width,
|
||||
offset,
|
||||
)
|
||||
}
|
||||
|
||||
// Black text in normal mode.
|
||||
pub fn cancel(
|
||||
with_outline: bool,
|
||||
with_arms: bool,
|
||||
force_width: Option<i16>,
|
||||
offset: Option<Offset>,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
theme::FG,
|
||||
theme::BG,
|
||||
with_outline,
|
||||
with_arms,
|
||||
force_width,
|
||||
offset,
|
||||
)
|
||||
// Self::new(theme::BG, theme::FG, with_outline, with_arms)
|
||||
}
|
||||
}
|
||||
|
||||
/// Describing the button in the choice item.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ButtonDetails<T> {
|
||||
pub text: Option<T>,
|
||||
pub icon: Option<Icon>,
|
||||
pub duration: Option<Duration>,
|
||||
pub is_cancel: bool,
|
||||
pub with_outline: bool,
|
||||
pub with_arms: bool,
|
||||
pub force_width: Option<i16>,
|
||||
pub offset: Option<Offset>,
|
||||
}
|
||||
|
||||
impl<T: Clone + AsRef<str>> ButtonDetails<T> {
|
||||
/// Text button.
|
||||
pub fn text(text: T) -> Self {
|
||||
Self {
|
||||
text: Some(text),
|
||||
icon: None,
|
||||
duration: None,
|
||||
is_cancel: false,
|
||||
with_outline: true,
|
||||
with_arms: false,
|
||||
force_width: None,
|
||||
offset: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Icon button.
|
||||
pub fn icon(icon: Icon) -> Self {
|
||||
Self {
|
||||
text: None,
|
||||
icon: Some(icon),
|
||||
duration: None,
|
||||
is_cancel: false,
|
||||
with_outline: true,
|
||||
with_arms: false,
|
||||
force_width: None,
|
||||
offset: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Text with arms signalling double press.
|
||||
pub fn armed_text(text: T) -> Self {
|
||||
Self::text(text).with_arms()
|
||||
}
|
||||
|
||||
/// Cross-style-icon cancel button with no outline.
|
||||
pub fn cancel_icon() -> Self {
|
||||
Self::icon(Icon::new(theme::ICON_CANCEL))
|
||||
.with_no_outline()
|
||||
.with_offset(Offset::new(2, -2))
|
||||
}
|
||||
|
||||
/// Left arrow to signal going back.
|
||||
pub fn left_arrow_icon() -> Self {
|
||||
Self::icon(Icon::new(theme::ICON_ARROW_LEFT)).with_no_outline()
|
||||
}
|
||||
|
||||
/// Right arrow to signal going forward.
|
||||
pub fn right_arrow_icon() -> Self {
|
||||
Self::icon(Icon::new(theme::ICON_ARROW_RIGHT)).with_no_outline()
|
||||
}
|
||||
|
||||
/// Down arrow to signal paginating forward. Takes half the screen's width
|
||||
pub fn down_arrow_icon_wide() -> Self {
|
||||
Self::icon(Icon::new(theme::ICON_ARROW_DOWN)).force_width(HALF_SCREEN_BUTTON_WIDTH)
|
||||
}
|
||||
|
||||
/// Up arrow to signal paginating back. Takes half the screen's width
|
||||
pub fn up_arrow_icon_wide() -> Self {
|
||||
Self::icon(Icon::new(theme::ICON_ARROW_UP)).force_width(HALF_SCREEN_BUTTON_WIDTH)
|
||||
}
|
||||
|
||||
/// Icon of a bin to signal deleting.
|
||||
pub fn bin_icon() -> Self {
|
||||
Self::icon(Icon::new(theme::ICON_BIN)).with_no_outline()
|
||||
}
|
||||
|
||||
/// Cancel style button.
|
||||
pub fn with_cancel(mut self) -> Self {
|
||||
self.is_cancel = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// No outline around the button.
|
||||
pub fn with_no_outline(mut self) -> Self {
|
||||
self.with_outline = false;
|
||||
self
|
||||
}
|
||||
|
||||
/// Positioning the icon precisely where we want.
|
||||
/// Buttons are by default placed exactly in the corners (left/right)
|
||||
/// or in the center in case of center button. The offset can change it.
|
||||
pub fn with_offset(mut self, offset: Offset) -> Self {
|
||||
self.offset = Some(offset);
|
||||
self
|
||||
}
|
||||
|
||||
/// Left and right "arms" around the button.
|
||||
/// Automatically disabling the outline.
|
||||
pub fn with_arms(mut self) -> Self {
|
||||
self.with_arms = true;
|
||||
self.with_outline = false;
|
||||
self
|
||||
}
|
||||
|
||||
/// Duration of the hold-to-confirm.
|
||||
pub fn with_duration(mut self, duration: Duration) -> Self {
|
||||
self.duration = Some(duration);
|
||||
self
|
||||
}
|
||||
|
||||
/// Width of the button.
|
||||
pub fn force_width(mut self, width: i16) -> Self {
|
||||
self.force_width = Some(width);
|
||||
self
|
||||
}
|
||||
|
||||
/// Button style that should be applied.
|
||||
pub fn style(&self) -> ButtonStyleSheet {
|
||||
if self.is_cancel {
|
||||
ButtonStyleSheet::cancel(
|
||||
self.with_outline,
|
||||
self.with_arms,
|
||||
self.force_width,
|
||||
self.offset,
|
||||
)
|
||||
} else {
|
||||
ButtonStyleSheet::default(
|
||||
self.with_outline,
|
||||
self.with_arms,
|
||||
self.force_width,
|
||||
self.offset,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Holding the button details for all three possible buttons.
|
||||
#[derive(Clone)]
|
||||
pub struct ButtonLayout<T> {
|
||||
pub btn_left: Option<ButtonDetails<T>>,
|
||||
pub btn_middle: Option<ButtonDetails<T>>,
|
||||
pub btn_right: Option<ButtonDetails<T>>,
|
||||
}
|
||||
|
||||
impl<T: AsRef<str>> ButtonLayout<T> {
|
||||
pub fn new(
|
||||
btn_left: Option<ButtonDetails<T>>,
|
||||
btn_middle: Option<ButtonDetails<T>>,
|
||||
btn_right: Option<ButtonDetails<T>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
btn_left,
|
||||
btn_middle,
|
||||
btn_right,
|
||||
}
|
||||
}
|
||||
|
||||
/// Empty layout for when we cannot yet tell which buttons
|
||||
/// should be on the screen.
|
||||
pub fn empty() -> Self {
|
||||
Self::new(None, None, None)
|
||||
}
|
||||
}
|
||||
|
||||
impl ButtonLayout<&'static str> {
|
||||
/// Default button layout for all three buttons - icons.
|
||||
pub fn default_three_icons() -> Self {
|
||||
Self::three_icons_middle_text("SELECT")
|
||||
}
|
||||
|
||||
/// Special middle text for default icon layout.
|
||||
pub fn three_icons_middle_text(middle_text: &'static str) -> Self {
|
||||
Self::new(
|
||||
Some(ButtonDetails::left_arrow_icon()),
|
||||
Some(ButtonDetails::armed_text(middle_text)),
|
||||
Some(ButtonDetails::right_arrow_icon()),
|
||||
)
|
||||
}
|
||||
|
||||
/// Left and right arrow icons for navigation.
|
||||
pub fn left_right_arrows() -> Self {
|
||||
Self::new(
|
||||
Some(ButtonDetails::left_arrow_icon()),
|
||||
None,
|
||||
Some(ButtonDetails::right_arrow_icon()),
|
||||
)
|
||||
}
|
||||
|
||||
/// Cancel cross on left and right arrow.
|
||||
pub fn cancel_and_arrow() -> Self {
|
||||
Self::new(
|
||||
Some(ButtonDetails::cancel_icon()),
|
||||
None,
|
||||
Some(ButtonDetails::right_arrow_icon()),
|
||||
)
|
||||
}
|
||||
|
||||
/// Cancel cross on left and text on the right.
|
||||
pub fn cancel_and_text(text: &'static str) -> Self {
|
||||
Self::new(
|
||||
Some(ButtonDetails::cancel_icon()),
|
||||
None,
|
||||
Some(ButtonDetails::text(text)),
|
||||
)
|
||||
}
|
||||
|
||||
/// Cancel cross on left and hold-to-confirm text on the right.
|
||||
pub fn cancel_and_htc_text(text: &'static str, duration: Duration) -> Self {
|
||||
Self::new(
|
||||
Some(ButtonDetails::cancel_icon()),
|
||||
None,
|
||||
Some(ButtonDetails::text(text).with_duration(duration)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// What happens when a button is triggered.
|
||||
/// Theoretically any action can be connected
|
||||
/// with any button.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub enum ButtonAction {
|
||||
/// Go to the next page of this flow
|
||||
NextPage,
|
||||
/// Go to the previous page of this flow
|
||||
PrevPage,
|
||||
/// Go to a page of this flow specified by an index.
|
||||
/// Negative numbers can be used to count from the end.
|
||||
/// (0 ~ GoToFirstPage, -1 ~ GoToLastPage etc.)
|
||||
GoToIndex(i16),
|
||||
/// Go forwards/backwards a specified number of pages.
|
||||
/// Negative numbers mean going back.
|
||||
MovePageRelative(i16),
|
||||
/// Cancel the whole layout - send Msg::Cancelled
|
||||
Cancel,
|
||||
/// Confirm the whole layout - send Msg::Confirmed
|
||||
Confirm,
|
||||
/// Select current choice value
|
||||
Select,
|
||||
/// Some custom specific action
|
||||
Action(&'static str),
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl ButtonAction {
|
||||
/// Describing the action as a string. Debug-only.
|
||||
pub fn string(&self) -> String<25> {
|
||||
match self {
|
||||
ButtonAction::NextPage => "Next".into(),
|
||||
ButtonAction::PrevPage => "Prev".into(),
|
||||
ButtonAction::GoToIndex(index) => {
|
||||
build_string!(25, "Index(", inttostr!(*index), ")")
|
||||
}
|
||||
ButtonAction::MovePageRelative(index) => {
|
||||
build_string!(25, "Relative(", inttostr!(*index), ")")
|
||||
}
|
||||
ButtonAction::Cancel => "Cancel".into(),
|
||||
ButtonAction::Confirm => "Confirm".into(),
|
||||
ButtonAction::Select => "Select".into(),
|
||||
ButtonAction::Action(action) => (*action).into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Adding a description to the Select action.
|
||||
pub fn select_item<T: AsRef<str>>(item: T) -> String<25> {
|
||||
build_string!(25, &Self::Select.string(), "(", item.as_ref(), ")")
|
||||
}
|
||||
|
||||
/// When there is no action.
|
||||
pub fn empty() -> String<25> {
|
||||
"None".into()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: might consider defining ButtonAction::Empty
|
||||
// and only storing ButtonAction instead of Option<ButtonAction>...
|
||||
|
||||
/// Storing actions for all three possible buttons.
|
||||
#[derive(Clone)]
|
||||
pub struct ButtonActions {
|
||||
pub left: Option<ButtonAction>,
|
||||
pub middle: Option<ButtonAction>,
|
||||
pub right: Option<ButtonAction>,
|
||||
}
|
||||
|
||||
impl ButtonActions {
|
||||
pub fn new(
|
||||
left: Option<ButtonAction>,
|
||||
middle: Option<ButtonAction>,
|
||||
right: Option<ButtonAction>,
|
||||
) -> Self {
|
||||
Self {
|
||||
left,
|
||||
middle,
|
||||
right,
|
||||
}
|
||||
}
|
||||
|
||||
/// Going back with left, going further with right
|
||||
pub fn prev_next() -> Self {
|
||||
Self::new(
|
||||
Some(ButtonAction::PrevPage),
|
||||
None,
|
||||
Some(ButtonAction::NextPage),
|
||||
)
|
||||
}
|
||||
|
||||
/// Going back with left, going further with middle
|
||||
pub fn prev_next_with_middle() -> Self {
|
||||
Self::new(
|
||||
Some(ButtonAction::PrevPage),
|
||||
Some(ButtonAction::NextPage),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
/// Going to last page with left, to the next page with right
|
||||
pub fn last_next() -> Self {
|
||||
Self::new(
|
||||
Some(ButtonAction::GoToIndex(-1)),
|
||||
None,
|
||||
Some(ButtonAction::NextPage),
|
||||
)
|
||||
}
|
||||
|
||||
/// Cancelling with left, going to the next page with right
|
||||
pub fn cancel_next() -> Self {
|
||||
Self::new(
|
||||
Some(ButtonAction::Cancel),
|
||||
None,
|
||||
Some(ButtonAction::NextPage),
|
||||
)
|
||||
}
|
||||
|
||||
/// Cancelling with left, confirming with right
|
||||
pub fn cancel_confirm() -> Self {
|
||||
Self::new(
|
||||
Some(ButtonAction::Cancel),
|
||||
None,
|
||||
Some(ButtonAction::Confirm),
|
||||
)
|
||||
}
|
||||
|
||||
/// Going to the beginning with left, confirming with right
|
||||
pub fn beginning_confirm() -> Self {
|
||||
Self::new(
|
||||
Some(ButtonAction::GoToIndex(0)),
|
||||
None,
|
||||
Some(ButtonAction::Confirm),
|
||||
)
|
||||
}
|
||||
|
||||
/// Going to the beginning with left, cancelling with right
|
||||
pub fn beginning_cancel() -> Self {
|
||||
Self::new(
|
||||
Some(ButtonAction::GoToIndex(0)),
|
||||
None,
|
||||
Some(ButtonAction::Cancel),
|
||||
)
|
||||
}
|
||||
|
||||
/// Having access to appropriate action based on the `ButtonPos`
|
||||
pub fn get_action(&self, pos: ButtonPos) -> Option<ButtonAction> {
|
||||
match pos {
|
||||
ButtonPos::Left => self.left.clone(),
|
||||
ButtonPos::Middle => self.middle.clone(),
|
||||
ButtonPos::Right => self.right.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T> crate::trace::Trace for ButtonDetails<T>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("ButtonDetails");
|
||||
let mut btn_text: String<30> = String::new();
|
||||
if let Some(text) = &self.text {
|
||||
btn_text.push_str(text.as_ref()).unwrap();
|
||||
} else if let Some(icon) = &self.icon {
|
||||
btn_text.push_str("Icon:").unwrap();
|
||||
btn_text.push_str(icon.text.as_ref()).unwrap();
|
||||
}
|
||||
if let Some(duration) = &self.duration {
|
||||
btn_text.push_str(" (HTC:").unwrap();
|
||||
btn_text.push_str(inttostr!(duration.to_millis())).unwrap();
|
||||
btn_text.push_str(")").unwrap();
|
||||
}
|
||||
t.button(btn_text.as_ref());
|
||||
t.close();
|
||||
}
|
||||
}
|
||||
|
449
core/embed/rust/src/ui/model_tr/component/button_controller.rs
Normal file
@ -0,0 +1,449 @@
|
||||
use super::{
|
||||
theme, Button, ButtonDetails, ButtonLayout, ButtonPos, HoldToConfirm, HoldToConfirmMsg,
|
||||
LoaderStyleSheet,
|
||||
};
|
||||
use crate::{
|
||||
time::Duration,
|
||||
ui::{
|
||||
component::{base::Event, Component, EventCtx, Pad},
|
||||
event::{ButtonEvent, PhysicalButton},
|
||||
geometry::Rect,
|
||||
},
|
||||
};
|
||||
|
||||
use heapless::String;
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
enum ButtonState {
|
||||
Nothing,
|
||||
OneDown(PhysicalButton),
|
||||
BothDown,
|
||||
OneReleased(PhysicalButton),
|
||||
HTCNeedsRelease(PhysicalButton),
|
||||
}
|
||||
|
||||
pub enum ButtonControllerMsg {
|
||||
Triggered(ButtonPos),
|
||||
}
|
||||
|
||||
/// Defines what kind of button should be currently used.
|
||||
pub enum ButtonType<T> {
|
||||
Button(Button<T>),
|
||||
HoldToConfirm(HoldToConfirm<T>),
|
||||
Nothing,
|
||||
}
|
||||
|
||||
impl<T> ButtonType<T>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
T: Clone,
|
||||
{
|
||||
pub fn from_button_details(pos: ButtonPos, btn_details: Option<ButtonDetails<T>>) -> Self {
|
||||
if let Some(btn_details) = btn_details {
|
||||
if btn_details.duration.is_some() {
|
||||
Self::HoldToConfirm(Self::get_hold_to_confirm(pos, btn_details))
|
||||
} else {
|
||||
Self::Button(Self::get_button(pos, btn_details))
|
||||
}
|
||||
} else {
|
||||
Self::Nothing
|
||||
}
|
||||
}
|
||||
|
||||
/// Create `Button` component from `btn_details`.
|
||||
fn get_button(pos: ButtonPos, btn_details: ButtonDetails<T>) -> Button<T> {
|
||||
// Deciding between text and icon
|
||||
if let Some(text) = btn_details.clone().text {
|
||||
Button::with_text(pos, text, btn_details.style())
|
||||
} else if let Some(icon) = btn_details.icon {
|
||||
Button::with_icon(pos, icon, btn_details.style())
|
||||
} else {
|
||||
panic!("ButtonContainer: no text or icon provided");
|
||||
}
|
||||
}
|
||||
|
||||
/// Create `HoldToConfirm` component from `btn_details`.
|
||||
fn get_hold_to_confirm(pos: ButtonPos, btn_details: ButtonDetails<T>) -> HoldToConfirm<T> {
|
||||
let duration = btn_details
|
||||
.duration
|
||||
.unwrap_or_else(|| Duration::from_millis(1000));
|
||||
if let Some(text) = btn_details.text {
|
||||
HoldToConfirm::text(pos, text, LoaderStyleSheet::default(), duration)
|
||||
} else if let Some(icon) = btn_details.icon {
|
||||
HoldToConfirm::icon(pos, icon, LoaderStyleSheet::default(), duration)
|
||||
} else {
|
||||
panic!("ButtonContainer: no text or icon provided");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn place(&mut self, button_area: Rect) {
|
||||
match self {
|
||||
Self::Button(button) => {
|
||||
button.place(button_area);
|
||||
}
|
||||
Self::HoldToConfirm(htc) => {
|
||||
htc.place(button_area);
|
||||
}
|
||||
Self::Nothing => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn paint(&mut self) {
|
||||
match self {
|
||||
Self::Button(button) => {
|
||||
button.paint();
|
||||
}
|
||||
Self::HoldToConfirm(htc) => {
|
||||
htc.paint();
|
||||
}
|
||||
Self::Nothing => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapping a button and its state, so that it can be easily
|
||||
/// controlled from outside.
|
||||
///
|
||||
/// Users have a choice of a normal button or Hold-to-confirm button.
|
||||
/// `button_type` specified what from those two is used, if anything.
|
||||
pub struct ButtonContainer<T> {
|
||||
pos: ButtonPos,
|
||||
button_type: ButtonType<T>,
|
||||
}
|
||||
|
||||
impl<T: Clone + AsRef<str>> ButtonContainer<T> {
|
||||
/// Supplying `None` as `btn_details` marks the button inactive
|
||||
/// (it can be later activated in `set()`).
|
||||
pub fn new(pos: ButtonPos, btn_details: Option<ButtonDetails<T>>) -> Self {
|
||||
Self {
|
||||
pos,
|
||||
button_type: ButtonType::from_button_details(pos, btn_details),
|
||||
}
|
||||
}
|
||||
|
||||
/// Changing the state of the button.
|
||||
///
|
||||
/// Passing `None` as `btn_details` will mark the button as inactive.
|
||||
pub fn set(&mut self, btn_details: Option<ButtonDetails<T>>, button_area: Rect) {
|
||||
self.button_type = ButtonType::from_button_details(self.pos, btn_details);
|
||||
self.button_type.place(button_area);
|
||||
}
|
||||
|
||||
/// Placing the possible component.
|
||||
pub fn place(&mut self, bounds: Rect) {
|
||||
self.button_type.place(bounds);
|
||||
}
|
||||
|
||||
/// Painting the component that should be currently visible, if any.
|
||||
pub fn paint(&mut self) {
|
||||
self.button_type.paint();
|
||||
}
|
||||
|
||||
/// Setting the visual state of the button - released/pressed.
|
||||
pub fn set_pressed(&mut self, ctx: &mut EventCtx, is_pressed: bool) {
|
||||
if let ButtonType::Button(btn) = &mut self.button_type {
|
||||
btn.set_pressed(ctx, is_pressed);
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether single-click should trigger action.
|
||||
pub fn reacts_to_single_click(&self) -> bool {
|
||||
matches!(self.button_type, ButtonType::Button(_))
|
||||
}
|
||||
|
||||
/// Find out whether hold-to-confirm was triggered.
|
||||
pub fn htc_got_triggered(&mut self, ctx: &mut EventCtx, event: Event) -> bool {
|
||||
if let ButtonType::HoldToConfirm(htc) = &mut self.button_type {
|
||||
if matches!(htc.event(ctx, event), Some(HoldToConfirmMsg::Confirmed)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Registering hold event.
|
||||
pub fn hold_started(&mut self, ctx: &mut EventCtx) {
|
||||
if let ButtonType::HoldToConfirm(htc) = &mut self.button_type {
|
||||
htc.event(ctx, Event::Button(ButtonEvent::HoldStarted));
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancelling hold event.
|
||||
pub fn hold_ended(&mut self, ctx: &mut EventCtx) {
|
||||
if let ButtonType::HoldToConfirm(htc) = &mut self.button_type {
|
||||
htc.event(ctx, Event::Button(ButtonEvent::HoldEnded));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Component responsible for handling buttons.
|
||||
///
|
||||
/// Acts as a state-machine of `ButtonState`.
|
||||
///
|
||||
/// Storing all three possible buttons - left, middle and right -
|
||||
/// and handling their placement, painting and returning
|
||||
/// appropriate events when they are triggered.
|
||||
///
|
||||
/// Buttons can be interactively changed by clients by `set()`.
|
||||
///
|
||||
/// Only "final" button events are returned in `ButtonControllerMsg::Triggered`,
|
||||
/// based upon the buttons being long-press or not.
|
||||
pub struct ButtonController<T> {
|
||||
pad: Pad,
|
||||
left_btn: ButtonContainer<T>,
|
||||
middle_btn: ButtonContainer<T>,
|
||||
right_btn: ButtonContainer<T>,
|
||||
state: ButtonState,
|
||||
// Button area is needed so the buttons
|
||||
// can be "re-placed" after their text is changed
|
||||
// Will be set in `place`
|
||||
button_area: Rect,
|
||||
}
|
||||
|
||||
impl<T: Clone + AsRef<str>> ButtonController<T> {
|
||||
pub fn new(btn_layout: ButtonLayout<T>) -> Self {
|
||||
Self {
|
||||
pad: Pad::with_background(theme::BG).with_clear(),
|
||||
left_btn: ButtonContainer::new(ButtonPos::Left, btn_layout.btn_left),
|
||||
middle_btn: ButtonContainer::new(ButtonPos::Middle, btn_layout.btn_middle),
|
||||
right_btn: ButtonContainer::new(ButtonPos::Right, btn_layout.btn_right),
|
||||
state: ButtonState::Nothing,
|
||||
button_area: Rect::zero(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Updating all the three buttons to the wanted states.
|
||||
pub fn set(&mut self, btn_layout: ButtonLayout<T>) {
|
||||
self.pad.clear();
|
||||
self.left_btn.set(btn_layout.btn_left, self.button_area);
|
||||
self.middle_btn.set(btn_layout.btn_middle, self.button_area);
|
||||
self.right_btn.set(btn_layout.btn_right, self.button_area);
|
||||
}
|
||||
|
||||
/// Setting the pressed state for all three buttons by boolean flags.
|
||||
fn set_pressed(&mut self, ctx: &mut EventCtx, left: bool, mid: bool, right: bool) {
|
||||
self.left_btn.set_pressed(ctx, left);
|
||||
self.middle_btn.set_pressed(ctx, mid);
|
||||
self.right_btn.set_pressed(ctx, right);
|
||||
}
|
||||
|
||||
/// Handle middle button hold-to-confirm start.
|
||||
/// We need to cancel possible holds in both other buttons.
|
||||
fn middle_hold_started(&mut self, ctx: &mut EventCtx) {
|
||||
self.left_btn.hold_ended(ctx);
|
||||
self.middle_btn.hold_started(ctx);
|
||||
self.right_btn.hold_ended(ctx);
|
||||
}
|
||||
|
||||
/// Handling the expiration of HTC elements.
|
||||
/// Finding out if they have been triggered and sending event
|
||||
/// for the appropriate button.
|
||||
/// Setting the state to wait for the appropriate release event
|
||||
/// from the pressed button. Also resetting visible state.
|
||||
fn handle_htc_expiration(
|
||||
&mut self,
|
||||
ctx: &mut EventCtx,
|
||||
event: Event,
|
||||
) -> Option<ButtonControllerMsg> {
|
||||
if self.left_btn.htc_got_triggered(ctx, event) {
|
||||
self.state = ButtonState::HTCNeedsRelease(PhysicalButton::Left);
|
||||
self.set_pressed(ctx, false, false, false);
|
||||
return Some(ButtonControllerMsg::Triggered(ButtonPos::Left));
|
||||
} else if self.middle_btn.htc_got_triggered(ctx, event) {
|
||||
// TODO: how to handle it here? Do we even need to?
|
||||
self.state = ButtonState::Nothing;
|
||||
self.set_pressed(ctx, false, false, false);
|
||||
return Some(ButtonControllerMsg::Triggered(ButtonPos::Middle));
|
||||
} else if self.right_btn.htc_got_triggered(ctx, event) {
|
||||
self.state = ButtonState::HTCNeedsRelease(PhysicalButton::Right);
|
||||
self.set_pressed(ctx, false, false, false);
|
||||
return Some(ButtonControllerMsg::Triggered(ButtonPos::Right));
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + AsRef<str>> Component for ButtonController<T> {
|
||||
type Msg = ButtonControllerMsg;
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
// State machine for the ButtonController
|
||||
// We are matching event with `Event::Button` for a button action
|
||||
// and `Event::Timer` for getting the expiration of HTC.
|
||||
match event {
|
||||
Event::Button(button) => {
|
||||
let (new_state, event) = match self.state {
|
||||
ButtonState::Nothing => match button {
|
||||
ButtonEvent::ButtonPressed(which) => {
|
||||
match which {
|
||||
PhysicalButton::Left => {
|
||||
self.left_btn.hold_started(ctx);
|
||||
}
|
||||
PhysicalButton::Right => {
|
||||
self.right_btn.hold_started(ctx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
(ButtonState::OneDown(which), None)
|
||||
}
|
||||
_ => (self.state, None),
|
||||
},
|
||||
ButtonState::OneDown(which_down) => match button {
|
||||
ButtonEvent::ButtonReleased(b) if b == which_down => match which_down {
|
||||
PhysicalButton::Left => (
|
||||
ButtonState::Nothing,
|
||||
if self.left_btn.reacts_to_single_click() {
|
||||
Some(ButtonControllerMsg::Triggered(ButtonPos::Left))
|
||||
} else {
|
||||
self.left_btn.hold_ended(ctx);
|
||||
None
|
||||
},
|
||||
),
|
||||
PhysicalButton::Right => (
|
||||
ButtonState::Nothing,
|
||||
if self.right_btn.reacts_to_single_click() {
|
||||
Some(ButtonControllerMsg::Triggered(ButtonPos::Right))
|
||||
} else {
|
||||
self.right_btn.hold_ended(ctx);
|
||||
None
|
||||
},
|
||||
),
|
||||
_ => (ButtonState::Nothing, None),
|
||||
},
|
||||
|
||||
ButtonEvent::ButtonPressed(b) if b != which_down => {
|
||||
self.middle_hold_started(ctx);
|
||||
(ButtonState::BothDown, None)
|
||||
}
|
||||
_ => (self.state, None),
|
||||
},
|
||||
ButtonState::BothDown => match button {
|
||||
ButtonEvent::ButtonReleased(b) => {
|
||||
self.middle_btn.hold_ended(ctx);
|
||||
(ButtonState::OneReleased(b), None)
|
||||
}
|
||||
_ => (self.state, None),
|
||||
},
|
||||
ButtonState::OneReleased(which_up) => match button {
|
||||
ButtonEvent::ButtonPressed(b) if b == which_up => {
|
||||
self.middle_hold_started(ctx);
|
||||
(ButtonState::BothDown, None)
|
||||
}
|
||||
ButtonEvent::ButtonReleased(b) if b != which_up => (
|
||||
ButtonState::Nothing,
|
||||
if self.middle_btn.reacts_to_single_click() {
|
||||
Some(ButtonControllerMsg::Triggered(ButtonPos::Middle))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
),
|
||||
_ => (self.state, None),
|
||||
},
|
||||
ButtonState::HTCNeedsRelease(needs_release) => match button {
|
||||
// Only going out of this state if correct button was released
|
||||
ButtonEvent::ButtonReleased(released) if needs_release == released => {
|
||||
(ButtonState::Nothing, None)
|
||||
}
|
||||
_ => (self.state, None),
|
||||
},
|
||||
};
|
||||
|
||||
// Updating the visual feedback for the buttons
|
||||
match new_state {
|
||||
// Not showing anything also when we wait for a release
|
||||
ButtonState::Nothing | ButtonState::HTCNeedsRelease(_) => {
|
||||
self.set_pressed(ctx, false, false, false);
|
||||
}
|
||||
ButtonState::OneDown(down_button) => match down_button {
|
||||
PhysicalButton::Left => {
|
||||
self.set_pressed(ctx, true, false, false);
|
||||
}
|
||||
PhysicalButton::Right => {
|
||||
self.set_pressed(ctx, false, false, true);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
ButtonState::BothDown | ButtonState::OneReleased(_) => {
|
||||
self.set_pressed(ctx, false, true, false);
|
||||
}
|
||||
};
|
||||
|
||||
self.state = new_state;
|
||||
event
|
||||
}
|
||||
Event::Timer(_) => self.handle_htc_expiration(ctx, event),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.pad.paint();
|
||||
self.left_btn.paint();
|
||||
self.middle_btn.paint();
|
||||
self.right_btn.paint();
|
||||
}
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
// Saving button area so that we can re-place the buttons
|
||||
// when they get updated
|
||||
self.button_area = bounds;
|
||||
|
||||
self.pad.place(bounds);
|
||||
self.left_btn.place(bounds);
|
||||
self.middle_btn.place(bounds);
|
||||
self.right_btn.place(bounds);
|
||||
|
||||
bounds
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
use super::ButtonContent;
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T> crate::trace::Trace for ButtonContainer<T>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("ButtonContainer");
|
||||
|
||||
// Putting together text representation of the button
|
||||
let mut btn_text: String<30> = String::new();
|
||||
if let ButtonType::Button(btn) = &self.button_type {
|
||||
match btn.content() {
|
||||
ButtonContent::Text(text) => {
|
||||
unwrap!(btn_text.push_str(text.as_ref()));
|
||||
}
|
||||
ButtonContent::Icon(icon) => {
|
||||
unwrap!(btn_text.push_str("Icon:"));
|
||||
unwrap!(btn_text.push_str(icon.text));
|
||||
}
|
||||
}
|
||||
} else if let ButtonType::HoldToConfirm(htc) = &self.button_type {
|
||||
unwrap!(btn_text.push_str(htc.get_text().as_ref()));
|
||||
unwrap!(btn_text.push_str(" (HTC:"));
|
||||
unwrap!(btn_text.push_str(inttostr!(htc.get_duration().to_millis())));
|
||||
unwrap!(btn_text.push_str(")"));
|
||||
} else {
|
||||
unwrap!(btn_text.push_str(crate::trace::EMPTY_BTN));
|
||||
}
|
||||
t.button(btn_text.as_ref());
|
||||
|
||||
t.close();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T> crate::trace::Trace for ButtonController<T>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("ButtonController");
|
||||
t.field("left_btn", &self.left_btn);
|
||||
t.field("middle_btn", &self.middle_btn);
|
||||
t.field("right_btn", &self.right_btn);
|
||||
t.close();
|
||||
}
|
||||
}
|
95
core/embed/rust/src/ui/model_tr/component/changing_text.rs
Normal file
@ -0,0 +1,95 @@
|
||||
use crate::ui::{
|
||||
component::{Component, Event, EventCtx, Never, Pad},
|
||||
display::Font,
|
||||
geometry::{Point, Rect},
|
||||
};
|
||||
|
||||
use super::{common, flow_pages_poc_helpers::LineAlignment, theme};
|
||||
|
||||
/// Component that allows for "allocating" a standalone line of text anywhere
|
||||
/// on the screen and updating it arbitrarily - without affecting the rest
|
||||
/// and without being affected by other components.
|
||||
pub struct ChangingTextLine<T> {
|
||||
area: Rect,
|
||||
pad: Pad,
|
||||
text: T,
|
||||
font: Font,
|
||||
line_alignment: LineAlignment,
|
||||
}
|
||||
|
||||
impl<T> ChangingTextLine<T>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
pub fn new(text: T, font: Font, line_alignment: LineAlignment) -> Self {
|
||||
Self {
|
||||
area: Rect::zero(),
|
||||
pad: Pad::with_background(theme::BG),
|
||||
text,
|
||||
font,
|
||||
line_alignment,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn center_mono(text: T) -> Self {
|
||||
Self::new(text, Font::MONO, LineAlignment::Center)
|
||||
}
|
||||
|
||||
pub fn update_text(&mut self, text: T) {
|
||||
self.text = text;
|
||||
self.pad.clear();
|
||||
}
|
||||
|
||||
/// Gets the height that is needed for this line to fit perfectly
|
||||
/// without affecting the rest of the screen.
|
||||
/// (Accounting for letters that go below the baseline (y, j, ...).)
|
||||
pub fn needed_height(&self) -> i16 {
|
||||
self.font.line_height() + 2
|
||||
}
|
||||
|
||||
/// Y coordinate of text baseline, is the same for all paints.
|
||||
fn y_baseline(&self) -> i16 {
|
||||
self.area.y0 + self.font.line_height()
|
||||
}
|
||||
|
||||
fn paint_left(&self) {
|
||||
let baseline = Point::new(self.area.x0, self.y_baseline());
|
||||
common::display(baseline, &self.text, self.font)
|
||||
}
|
||||
|
||||
fn paint_center(&self) {
|
||||
let baseline = Point::new(self.area.bottom_center().x, self.y_baseline());
|
||||
common::display_center(baseline, &self.text, self.font)
|
||||
}
|
||||
|
||||
fn paint_right(&self) {
|
||||
let baseline = Point::new(self.area.x1, self.y_baseline());
|
||||
common::display_right(baseline, &self.text, self.font)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Component for ChangingTextLine<T>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
type Msg = Never;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.area = bounds;
|
||||
self.pad.place(bounds);
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.pad.paint();
|
||||
match self.line_alignment {
|
||||
LineAlignment::Left => self.paint_left(),
|
||||
LineAlignment::Center => self.paint_center(),
|
||||
LineAlignment::Right => self.paint_right(),
|
||||
}
|
||||
}
|
||||
}
|
326
core/embed/rust/src/ui/model_tr/component/choice.rs
Normal file
@ -0,0 +1,326 @@
|
||||
use crate::ui::{
|
||||
component::{Child, Component, Event, EventCtx, Pad},
|
||||
geometry::Rect,
|
||||
};
|
||||
|
||||
use super::{theme, ButtonController, ButtonControllerMsg, ButtonPos, ChoiceItem, ChoiceItemAPI};
|
||||
|
||||
pub enum ChoicePageMsg {
|
||||
Choice(u8),
|
||||
LeftMost,
|
||||
RightMost,
|
||||
}
|
||||
|
||||
const MIDDLE_ROW: i32 = 72;
|
||||
|
||||
/// Interface for a specific component efficiently giving
|
||||
/// `ChoicePage` all the information it needs to render
|
||||
/// all the choice pages.
|
||||
///
|
||||
/// It avoids the need to store the whole sequence of
|
||||
/// `ChoiceItem`s in `heapless::Vec` (which caused StackOverflow),
|
||||
/// but offers a "lazy-loading" way of requesting the
|
||||
/// `ChoiceItem`s only when they are needed, one-by-one.
|
||||
/// This way, no more than one `ChoiceItem` is stored in memory at any time.
|
||||
pub trait ChoiceFactory {
|
||||
fn get(&self, choice_index: u8) -> ChoiceItem;
|
||||
fn count(&self) -> u8;
|
||||
}
|
||||
|
||||
/// General component displaying a set of items on the screen
|
||||
/// and allowing the user to select one of them.
|
||||
///
|
||||
/// To be used by other more specific components that will
|
||||
/// supply a set of `ChoiceItem`s (through `ChoiceFactory`)
|
||||
/// and will receive back the index of the selected choice.
|
||||
///
|
||||
/// Each `ChoiceItem` is responsible for setting the screen -
|
||||
/// choosing the button text, their duration, text displayed
|
||||
/// on screen etc.
|
||||
///
|
||||
/// `is_carousel` can be used to make the choice page "infinite" -
|
||||
/// after reaching one end, users will appear at the other end.
|
||||
pub struct ChoicePage<F>
|
||||
where
|
||||
F: ChoiceFactory,
|
||||
{
|
||||
choices: F,
|
||||
pad: Pad,
|
||||
buttons: Child<ButtonController<&'static str>>,
|
||||
page_counter: u8,
|
||||
is_carousel: bool,
|
||||
}
|
||||
|
||||
impl<F> ChoicePage<F>
|
||||
where
|
||||
F: ChoiceFactory,
|
||||
{
|
||||
pub fn new(choices: F) -> Self {
|
||||
let initial_btn_layout = choices.get(0).btn_layout();
|
||||
|
||||
Self {
|
||||
choices,
|
||||
pad: Pad::with_background(theme::BG),
|
||||
buttons: Child::new(ButtonController::new(initial_btn_layout)),
|
||||
page_counter: 0,
|
||||
is_carousel: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the page counter at the very beginning.
|
||||
pub fn with_initial_page_counter(mut self, page_counter: u8) -> Self {
|
||||
self.page_counter = page_counter;
|
||||
self
|
||||
}
|
||||
|
||||
/// Enabling the carousel mode.
|
||||
pub fn with_carousel(mut self) -> Self {
|
||||
self.is_carousel = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Resetting the component, which enables reusing the same instance
|
||||
/// for multiple choice categories.
|
||||
///
|
||||
/// NOTE: from the client point of view, it would also be an option to
|
||||
/// always create a new instance with fresh setup, but I could not manage to
|
||||
/// properly clean up the previous instance - it would still be shown on
|
||||
/// screen and colliding with the new one.
|
||||
pub fn reset(
|
||||
&mut self,
|
||||
ctx: &mut EventCtx,
|
||||
new_choices: F,
|
||||
reset_page_counter: bool,
|
||||
is_carousel: bool,
|
||||
) {
|
||||
self.choices = new_choices;
|
||||
if reset_page_counter {
|
||||
self.page_counter = 0;
|
||||
}
|
||||
self.update(ctx);
|
||||
self.is_carousel = is_carousel;
|
||||
}
|
||||
|
||||
/// Navigating to the chosen page index.
|
||||
pub fn set_page_counter(&mut self, ctx: &mut EventCtx, page_counter: u8) {
|
||||
self.page_counter = page_counter;
|
||||
self.update(ctx);
|
||||
}
|
||||
|
||||
/// Display current, previous and next choice according to
|
||||
/// the current ChoiceItem.
|
||||
fn paint_choices(&mut self) {
|
||||
// Performing the appropriate `paint_XXX()` for the main choice
|
||||
// and two adjacent choices when present
|
||||
// In case of carousel mode, also showing the ones from other end.
|
||||
self.show_current_choice();
|
||||
|
||||
if self.has_previous_choice() {
|
||||
self.show_previous_choice();
|
||||
} else if self.is_carousel {
|
||||
self.show_last_choice_on_left();
|
||||
}
|
||||
|
||||
if self.has_next_choice() {
|
||||
self.show_next_choice();
|
||||
} else if self.is_carousel {
|
||||
self.show_first_choice_on_right();
|
||||
}
|
||||
}
|
||||
|
||||
/// Setting current buttons, and clearing.
|
||||
fn update(&mut self, ctx: &mut EventCtx) {
|
||||
self.set_buttons(ctx);
|
||||
self.clear(ctx);
|
||||
}
|
||||
|
||||
/// Clearing the whole area and requesting repaint.
|
||||
fn clear(&mut self, ctx: &mut EventCtx) {
|
||||
self.pad.clear();
|
||||
ctx.request_paint();
|
||||
}
|
||||
|
||||
fn last_page_index(&self) -> u8 {
|
||||
self.choices.count() as u8 - 1
|
||||
}
|
||||
|
||||
pub fn has_previous_choice(&self) -> bool {
|
||||
self.page_counter > 0
|
||||
}
|
||||
|
||||
pub fn has_next_choice(&self) -> bool {
|
||||
self.page_counter < self.last_page_index()
|
||||
}
|
||||
|
||||
fn current_choice(&self) -> ChoiceItem {
|
||||
self.get_choice(self.page_counter)
|
||||
}
|
||||
|
||||
fn get_choice(&self, index: u8) -> ChoiceItem {
|
||||
self.choices.get(index)
|
||||
}
|
||||
|
||||
fn show_current_choice(&self) {
|
||||
self.current_choice().paint_center();
|
||||
}
|
||||
|
||||
fn show_previous_choice(&self) {
|
||||
self.get_choice(self.page_counter - 1).paint_left();
|
||||
}
|
||||
|
||||
fn show_next_choice(&self) {
|
||||
self.get_choice(self.page_counter + 1).paint_right();
|
||||
}
|
||||
|
||||
fn show_last_choice_on_left(&self) {
|
||||
self.get_choice(self.last_page_index()).paint_left();
|
||||
}
|
||||
|
||||
fn show_first_choice_on_right(&self) {
|
||||
self.get_choice(0).paint_right();
|
||||
}
|
||||
|
||||
fn decrease_page_counter(&mut self) {
|
||||
self.page_counter -= 1;
|
||||
}
|
||||
|
||||
fn increase_page_counter(&mut self) {
|
||||
self.page_counter += 1;
|
||||
}
|
||||
|
||||
fn page_counter_to_zero(&mut self) {
|
||||
self.page_counter = 0;
|
||||
}
|
||||
|
||||
fn page_counter_to_max(&mut self) {
|
||||
self.page_counter = self.last_page_index();
|
||||
}
|
||||
|
||||
pub fn page_index(&self) -> u8 {
|
||||
self.page_counter
|
||||
}
|
||||
|
||||
/// Updating the visual state of the buttons after each event.
|
||||
/// All three buttons are handled based upon the current choice.
|
||||
/// If defined in the current choice, setting their text,
|
||||
/// whether they are long-pressed, and painting them.
|
||||
///
|
||||
/// NOTE: ButtonController is handling the painting, and
|
||||
/// it will not repaint the buttons unless some of them changed.
|
||||
fn set_buttons(&mut self, ctx: &mut EventCtx) {
|
||||
// TODO: offer the possibility to change the buttons from the client
|
||||
// (button details could be changed in the same index)
|
||||
// Use-case: BIN button in PIN is deleting last digit if the PIN is not empty,
|
||||
// otherwise causing Cancel. Would be nice to allow deleting as a single click
|
||||
// and Cancel as HTC. PIN client would check if the PIN is empty/not and
|
||||
// adjust the HTC/not.
|
||||
|
||||
let btn_layout = self.current_choice().btn_layout();
|
||||
self.buttons.mutate(ctx, |_ctx, buttons| {
|
||||
buttons.set(btn_layout);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> Component for ChoicePage<F>
|
||||
where
|
||||
F: ChoiceFactory,
|
||||
{
|
||||
type Msg = ChoicePageMsg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
let (content_area, button_area) = bounds.split_bottom(theme::BUTTON_HEIGHT);
|
||||
self.pad.place(content_area);
|
||||
self.buttons.place(button_area);
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
let button_event = self.buttons.event(ctx, event);
|
||||
|
||||
if let Some(ButtonControllerMsg::Triggered(pos)) = button_event {
|
||||
match pos {
|
||||
ButtonPos::Left => {
|
||||
if self.has_previous_choice() {
|
||||
// Clicked BACK. Decrease the page counter.
|
||||
self.decrease_page_counter();
|
||||
self.update(ctx);
|
||||
} else if self.is_carousel {
|
||||
// In case of carousel going to the right end.
|
||||
self.page_counter_to_max();
|
||||
self.update(ctx);
|
||||
} else {
|
||||
// Triggered LEFTmost button. Send event
|
||||
self.clear(ctx);
|
||||
return Some(ChoicePageMsg::LeftMost);
|
||||
}
|
||||
}
|
||||
ButtonPos::Right => {
|
||||
if self.has_next_choice() {
|
||||
// Clicked NEXT. Increase the page counter.
|
||||
self.increase_page_counter();
|
||||
self.update(ctx);
|
||||
} else if self.is_carousel {
|
||||
// In case of carousel going to the left end.
|
||||
self.page_counter_to_zero();
|
||||
self.update(ctx);
|
||||
} else {
|
||||
// Triggered RIGHTmost button. Send event
|
||||
self.clear(ctx);
|
||||
return Some(ChoicePageMsg::RightMost);
|
||||
}
|
||||
}
|
||||
ButtonPos::Middle => {
|
||||
// Clicked SELECT. Send current choice index
|
||||
self.clear(ctx);
|
||||
return Some(ChoicePageMsg::Choice(self.page_counter));
|
||||
}
|
||||
}
|
||||
};
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.pad.paint();
|
||||
self.buttons.paint();
|
||||
self.paint_choices();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<F> crate::trace::Trace for ChoicePage<F>
|
||||
where
|
||||
F: ChoiceFactory,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("ChoicePage");
|
||||
t.kw_pair("active_page", inttostr!(self.page_counter));
|
||||
t.kw_pair("page_count", inttostr!(self.choices.count() as u8));
|
||||
t.kw_pair("is_carousel", booltostr!(self.is_carousel));
|
||||
|
||||
if self.has_previous_choice() {
|
||||
t.field("prev_choice", &self.get_choice(self.page_counter - 1));
|
||||
} else if self.is_carousel {
|
||||
// In case of carousel going to the left end.
|
||||
t.field("prev_choice", &self.get_choice(self.last_page_index()));
|
||||
} else {
|
||||
t.string("prev_choice");
|
||||
t.symbol("None");
|
||||
}
|
||||
|
||||
t.field("current_choice", &self.current_choice());
|
||||
|
||||
if self.has_next_choice() {
|
||||
t.field("next_choice", &self.get_choice(self.page_counter + 1));
|
||||
} else if self.is_carousel {
|
||||
// In case of carousel going to the very left.
|
||||
t.field("next_choice", &self.get_choice(0));
|
||||
} else {
|
||||
t.string("next_choice");
|
||||
t.symbol("None");
|
||||
}
|
||||
|
||||
t.field("buttons", &self.buttons);
|
||||
t.close();
|
||||
}
|
||||
}
|
338
core/embed/rust/src/ui/model_tr/component/choice_item.rs
Normal file
@ -0,0 +1,338 @@
|
||||
use crate::ui::{geometry::Point, display::Font, util::char_to_string};
|
||||
use heapless::String;
|
||||
|
||||
use super::{
|
||||
common::{display, display_center, display_right},
|
||||
ButtonDetails, ButtonLayout,
|
||||
};
|
||||
|
||||
const MIDDLE_ROW: i16 = 61;
|
||||
const LEFT_COL: i16 = 1;
|
||||
const MIDDLE_COL: i16 = 64;
|
||||
const RIGHT_COL: i16 = 127;
|
||||
|
||||
/// Helper to unite the row height.
|
||||
fn row_height() -> i16 {
|
||||
// It never reaches the maximum height
|
||||
Font::NORMAL.line_height() - 4
|
||||
}
|
||||
|
||||
/// Component that can be used as a choice item.
|
||||
/// Allows to have a choice of anything that can be painted on screen.
|
||||
///
|
||||
/// Controls the painting of the current, previous and next item
|
||||
/// through `paint_XXX()` methods.
|
||||
/// Defines the behavior of all three buttons through `btn_XXX` attributes.
|
||||
///
|
||||
/// Possible implementations:
|
||||
/// - [x] `TextChoiceItem` - for regular text
|
||||
/// - [x] `MultilineTextChoiceItem` - for multiline text
|
||||
/// - [x] `BigCharacterChoiceItem` - for one big character
|
||||
/// - [ ] `IconChoiceItem` - for showing icons
|
||||
/// - [ ] `JustCenterChoice` - paint_left() and paint_right() show nothing
|
||||
/// - [ ] `LongStringsChoice` - paint_left() and paint_right() show ellipsis
|
||||
pub trait ChoiceItemAPI {
|
||||
fn paint_center(&mut self);
|
||||
fn paint_left(&mut self);
|
||||
fn paint_right(&mut self);
|
||||
fn btn_layout(&self) -> ButtonLayout<&'static str>;
|
||||
}
|
||||
|
||||
// TODO: consider having
|
||||
// pub trait ChoiceItemOperations {}
|
||||
|
||||
// TODO: consider storing all the text components as `T: AsRef<str>`
|
||||
// Tried, but it makes the code unnecessarily messy with all the <T>
|
||||
// definitions, which needs to be added to all the components using it.
|
||||
|
||||
/// Storing all the possible implementations of `ChoiceItemAPI`.
|
||||
/// Done like this as we want to use multiple different choice pages
|
||||
/// at the same time in `ChoicePage` - for example Multiline and BigLetters
|
||||
#[derive(Clone)]
|
||||
pub enum ChoiceItem {
|
||||
Text(TextChoiceItem),
|
||||
MultilineText(MultilineTextChoiceItem),
|
||||
BigCharacter(BigCharacterChoiceItem),
|
||||
}
|
||||
|
||||
impl ChoiceItem {
|
||||
// TODO: can we somehow avoid the repetitions here?
|
||||
pub fn set_left_btn(&mut self, btn_left: Option<ButtonDetails<&'static str>>) {
|
||||
match self {
|
||||
ChoiceItem::Text(item) => item.btn_layout.btn_left = btn_left,
|
||||
ChoiceItem::MultilineText(item) => item.btn_layout.btn_left = btn_left,
|
||||
ChoiceItem::BigCharacter(item) => item.btn_layout.btn_left = btn_left,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_middle_btn(&mut self, btn_middle: Option<ButtonDetails<&'static str>>) {
|
||||
match self {
|
||||
ChoiceItem::Text(item) => item.btn_layout.btn_middle = btn_middle,
|
||||
ChoiceItem::MultilineText(item) => item.btn_layout.btn_middle = btn_middle,
|
||||
ChoiceItem::BigCharacter(item) => item.btn_layout.btn_middle = btn_middle,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_right_btn(&mut self, btn_right: Option<ButtonDetails<&'static str>>) {
|
||||
match self {
|
||||
ChoiceItem::Text(item) => item.btn_layout.btn_right = btn_right,
|
||||
ChoiceItem::MultilineText(item) => item.btn_layout.btn_right = btn_right,
|
||||
ChoiceItem::BigCharacter(item) => item.btn_layout.btn_right = btn_right,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_text(&mut self, text: String<50>) {
|
||||
match self {
|
||||
ChoiceItem::Text(item) => item.text = text,
|
||||
ChoiceItem::MultilineText(item) => item.text = text,
|
||||
ChoiceItem::BigCharacter(_) => {
|
||||
panic!("No text setting for BigCharacter")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ChoiceItemAPI for ChoiceItem {
|
||||
fn paint_center(&mut self) {
|
||||
match self {
|
||||
ChoiceItem::Text(item) => item.paint_center(),
|
||||
ChoiceItem::MultilineText(item) => item.paint_center(),
|
||||
ChoiceItem::BigCharacter(item) => item.paint_center(),
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_left(&mut self) {
|
||||
match self {
|
||||
ChoiceItem::Text(item) => item.paint_left(),
|
||||
ChoiceItem::MultilineText(item) => item.paint_left(),
|
||||
ChoiceItem::BigCharacter(item) => item.paint_left(),
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_right(&mut self) {
|
||||
match self {
|
||||
ChoiceItem::Text(item) => item.paint_right(),
|
||||
ChoiceItem::MultilineText(item) => item.paint_right(),
|
||||
ChoiceItem::BigCharacter(item) => item.paint_right(),
|
||||
}
|
||||
}
|
||||
|
||||
fn btn_layout(&self) -> ButtonLayout<&'static str> {
|
||||
match self {
|
||||
ChoiceItem::Text(item) => item.btn_layout(),
|
||||
ChoiceItem::MultilineText(item) => item.btn_layout(),
|
||||
ChoiceItem::BigCharacter(item) => item.btn_layout(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple string component used as a choice item.
|
||||
#[derive(Clone)]
|
||||
pub struct TextChoiceItem {
|
||||
pub text: String<50>,
|
||||
pub btn_layout: ButtonLayout<&'static str>,
|
||||
}
|
||||
|
||||
impl TextChoiceItem {
|
||||
pub fn new<T>(text: T, btn_layout: ButtonLayout<&'static str>) -> Self
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
Self {
|
||||
text: String::from(text.as_ref()),
|
||||
btn_layout,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ChoiceItemAPI for TextChoiceItem {
|
||||
fn paint_center(&mut self) {
|
||||
// Displaying the center choice lower than the rest,
|
||||
// to make it more clear this is the current choice
|
||||
// (and also the left and right ones do not collide with it)
|
||||
display_center(
|
||||
Point::new(MIDDLE_COL, MIDDLE_ROW + row_height()),
|
||||
&self.text,
|
||||
Font::NORMAL,
|
||||
);
|
||||
}
|
||||
|
||||
fn paint_left(&mut self) {
|
||||
display(
|
||||
Point::new(LEFT_COL, MIDDLE_ROW),
|
||||
&self.text,
|
||||
Font::NORMAL,
|
||||
);
|
||||
}
|
||||
|
||||
fn paint_right(&mut self) {
|
||||
display_right(
|
||||
Point::new(RIGHT_COL, MIDDLE_ROW),
|
||||
&self.text,
|
||||
Font::NORMAL,
|
||||
);
|
||||
}
|
||||
|
||||
fn btn_layout(&self) -> ButtonLayout<&'static str> {
|
||||
self.btn_layout.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Multiline string component used as a choice item.
|
||||
///
|
||||
/// Lines are delimited by '\n' character, unless specified explicitly.
|
||||
#[derive(Clone)]
|
||||
pub struct MultilineTextChoiceItem {
|
||||
// Arbitrary chosen. TODO: agree on this
|
||||
pub text: String<50>,
|
||||
delimiter: char,
|
||||
pub btn_layout: ButtonLayout<&'static str>,
|
||||
}
|
||||
|
||||
impl MultilineTextChoiceItem {
|
||||
pub fn new(text: String<50>, btn_layout: ButtonLayout<&'static str>) -> Self {
|
||||
Self {
|
||||
text,
|
||||
delimiter: '\n',
|
||||
btn_layout,
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows for changing the line delimiter to arbitrary char.
|
||||
pub fn use_delimiter(mut self, delimiter: char) -> Self {
|
||||
self.delimiter = delimiter;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Make all the text be centered vertically - account for amount of lines.
|
||||
impl ChoiceItemAPI for MultilineTextChoiceItem {
|
||||
fn paint_center(&mut self) {
|
||||
// Displaying the center choice lower than the rest,
|
||||
// to make it more clear this is the current choice
|
||||
for (index, line) in self.text.split(self.delimiter).enumerate() {
|
||||
let offset = MIDDLE_ROW + index as i16 * row_height() + row_height();
|
||||
display_center(Point::new(MIDDLE_COL, offset), &line, Font::NORMAL);
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_left(&mut self) {
|
||||
for (index, line) in self.text.split(self.delimiter).enumerate() {
|
||||
let offset = MIDDLE_ROW + index as i16 * row_height();
|
||||
display(Point::new(LEFT_COL, offset), &line, Font::NORMAL);
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_right(&mut self) {
|
||||
for (index, line) in self.text.split(self.delimiter).enumerate() {
|
||||
let offset = MIDDLE_ROW + index as i16 * row_height();
|
||||
display_right(Point::new(RIGHT_COL, offset), &line, Font::NORMAL);
|
||||
}
|
||||
}
|
||||
|
||||
fn btn_layout(&self) -> ButtonLayout<&'static str> {
|
||||
self.btn_layout.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Choice item displaying single characters in BIG font.
|
||||
/// Middle choice is magnified 4 times, left and right 2 times.
|
||||
#[derive(Clone)]
|
||||
pub struct BigCharacterChoiceItem {
|
||||
pub ch: char,
|
||||
pub btn_layout: ButtonLayout<&'static str>,
|
||||
}
|
||||
|
||||
impl BigCharacterChoiceItem {
|
||||
pub fn new(ch: char, btn_layout: ButtonLayout<&'static str>) -> Self {
|
||||
Self { ch, btn_layout }
|
||||
}
|
||||
|
||||
/// Taking the first character from the `text`.
|
||||
pub fn from_str<T>(text: T, btn_layout: ButtonLayout<&'static str>) -> Self
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
Self {
|
||||
ch: text.as_ref().chars().next().unwrap(),
|
||||
btn_layout,
|
||||
}
|
||||
}
|
||||
|
||||
fn _paint_char(&mut self, baseline: Point) {
|
||||
display(
|
||||
baseline,
|
||||
&char_to_string::<1>(self.ch),
|
||||
Font::NORMAL,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl ChoiceItemAPI for BigCharacterChoiceItem {
|
||||
fn paint_center(&mut self) {
|
||||
self._paint_char(Point::new(MIDDLE_COL - 12, MIDDLE_ROW + 9));
|
||||
}
|
||||
|
||||
fn paint_left(&mut self) {
|
||||
self._paint_char(Point::new(LEFT_COL, MIDDLE_ROW));
|
||||
}
|
||||
|
||||
fn paint_right(&mut self) {
|
||||
self._paint_char(Point::new(RIGHT_COL - 12, MIDDLE_ROW));
|
||||
}
|
||||
|
||||
fn btn_layout(&self) -> ButtonLayout<&'static str> {
|
||||
self.btn_layout.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for ChoiceItem {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("ChoiceItem");
|
||||
match self {
|
||||
ChoiceItem::Text(item) => item.trace(t),
|
||||
ChoiceItem::MultilineText(item) => item.trace(t),
|
||||
ChoiceItem::BigCharacter(item) => item.trace(t),
|
||||
}
|
||||
t.close();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for TextChoiceItem {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("TextChoiceItem");
|
||||
t.content_flag();
|
||||
t.string(&self.text);
|
||||
t.content_flag();
|
||||
t.close();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
use crate::ui::util;
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for MultilineTextChoiceItem {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("MultilineTextChoiceItem");
|
||||
t.content_flag();
|
||||
t.string(&self.text);
|
||||
t.content_flag();
|
||||
t.field("delimiter", &(util::char_to_string::<1>(self.delimiter)));
|
||||
t.close();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for BigCharacterChoiceItem {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("BigCharacterChoiceItem");
|
||||
t.content_flag();
|
||||
t.string(&util::char_to_string::<1>(self.ch));
|
||||
t.content_flag();
|
||||
t.close();
|
||||
}
|
||||
}
|
120
core/embed/rust/src/ui/model_tr/component/common.rs
Normal file
@ -0,0 +1,120 @@
|
||||
use crate::ui::{
|
||||
display::{self, Font, Icon},
|
||||
geometry::{Offset, Point},
|
||||
model_tr::constant,
|
||||
};
|
||||
|
||||
use heapless::String;
|
||||
|
||||
use super::theme;
|
||||
|
||||
/// Display header text.
|
||||
pub fn display_header<T: AsRef<str>>(baseline: Point, text: T) {
|
||||
// TODO: make this centered?
|
||||
display::text(
|
||||
baseline,
|
||||
text.as_ref(),
|
||||
theme::FONT_HEADER,
|
||||
theme::FG,
|
||||
theme::BG,
|
||||
);
|
||||
}
|
||||
|
||||
/// Display bold white text on black background
|
||||
pub fn display<T: AsRef<str>>(baseline: Point, text: &T, font: Font) {
|
||||
display::text(baseline, text.as_ref(), font, theme::FG, theme::BG);
|
||||
}
|
||||
|
||||
/// Display white text on black background,
|
||||
/// centered around a baseline Point
|
||||
pub fn display_center<T: AsRef<str>>(baseline: Point, text: &T, font: Font) {
|
||||
display::text_center(baseline, text.as_ref(), font, theme::FG, theme::BG);
|
||||
}
|
||||
|
||||
/// Display white text on black background,
|
||||
/// with right boundary at a baseline Point
|
||||
pub fn display_right<T: AsRef<str>>(baseline: Point, text: &T, font: Font) {
|
||||
display::text_right(baseline, text.as_ref(), font, theme::FG, theme::BG);
|
||||
}
|
||||
|
||||
const MAX_VISIBLE_CHARS: usize = 18;
|
||||
const TOP_ROW_TEXT: i16 = 7;
|
||||
|
||||
/// Display indication of current user input - PIN, passphrase etc.
|
||||
/// Showing one asterisk for each character of the input.
|
||||
pub fn display_dots_center_top(dots_amount: usize, offset_from_top: i16) {
|
||||
let y_position = TOP_ROW_TEXT + offset_from_top;
|
||||
let dots_visible = dots_amount.min(MAX_VISIBLE_CHARS);
|
||||
|
||||
// String::repeat() is not available for heapless::String
|
||||
let mut dots: String<MAX_VISIBLE_CHARS> = String::new();
|
||||
for _ in 0..dots_visible {
|
||||
dots.push_str("*").unwrap();
|
||||
}
|
||||
|
||||
// Giving some notion of change even for longer-than-visible passphrases
|
||||
// - slightly shifting the dots to the left and right after each new digit
|
||||
if dots_amount > MAX_VISIBLE_CHARS && dots_amount % 2 == 0 {
|
||||
display_center(Point::new(61, y_position), &dots, Font::MONO);
|
||||
} else {
|
||||
display_center(Point::new(64, y_position), &dots, Font::MONO);
|
||||
}
|
||||
}
|
||||
|
||||
/// Display secret input that user is currently doing - PIN, passphrase etc.
|
||||
pub fn display_secret_center_top<T: AsRef<str>>(secret: T, offset_from_top: i16) {
|
||||
let y_position = TOP_ROW_TEXT + offset_from_top;
|
||||
let char_amount = secret.as_ref().len();
|
||||
if char_amount <= MAX_VISIBLE_CHARS {
|
||||
display_center(Point::new(64, y_position), &secret, Font::MONO);
|
||||
} else {
|
||||
// Show the last part with preceding ellipsis to show something is hidden
|
||||
let ellipsis = "...";
|
||||
let offset: usize = char_amount.saturating_sub(MAX_VISIBLE_CHARS) + ellipsis.len();
|
||||
let to_show = build_string!(MAX_VISIBLE_CHARS, ellipsis, &secret.as_ref()[offset..]);
|
||||
display_center(Point::new(64, y_position), &to_show, Font::MONO);
|
||||
}
|
||||
}
|
||||
|
||||
/// Display title and possible subtitle together with a dotted line spanning
|
||||
/// the entire width.
|
||||
/// Returning the painted height of the whole header.
|
||||
pub fn paint_header<T: AsRef<str>>(top_left: Point, title: T, subtitle: Option<T>) -> i16 {
|
||||
let text_heigth = theme::FONT_HEADER.text_height();
|
||||
let title_baseline = top_left + Offset::y(text_heigth);
|
||||
display_header(title_baseline, title);
|
||||
// Optionally painting the subtitle as well
|
||||
// (and offsetting the dotted line in that case)
|
||||
let mut dotted_line_offset = text_heigth + 2;
|
||||
if let Some(subtitle) = subtitle {
|
||||
dotted_line_offset += text_heigth;
|
||||
display_header(title_baseline + Offset::y(text_heigth), subtitle);
|
||||
}
|
||||
let line_start = top_left + Offset::y(dotted_line_offset);
|
||||
display::dotted_line_horizontal(line_start, constant::WIDTH, theme::FG, 2);
|
||||
dotted_line_offset
|
||||
}
|
||||
|
||||
/// Draws icon and text on the same line - icon on the left.
|
||||
pub fn icon_with_text<T: AsRef<str>>(baseline: Point, icon: Icon, text: T, font: Font) {
|
||||
icon.draw_bottom_left(baseline, theme::FG, theme::BG);
|
||||
let text_x_offset = icon.width() + 2;
|
||||
display(baseline + Offset::x(text_x_offset), &text.as_ref(), font);
|
||||
}
|
||||
|
||||
/// Draw two lines - icon with label text (key) and another text (value) below.
|
||||
/// Returns the height painted below the given baseline.
|
||||
pub fn key_value_icon<T: AsRef<str>>(
|
||||
baseline: Point,
|
||||
icon: Icon,
|
||||
label: T,
|
||||
label_font: Font,
|
||||
value: T,
|
||||
value_font: Font,
|
||||
) -> i16 {
|
||||
icon_with_text(baseline, icon, label, label_font);
|
||||
let line_height = value_font.line_height();
|
||||
let next_line = baseline + Offset::y(line_height);
|
||||
display(next_line, &value, value_font);
|
||||
line_height
|
||||
}
|
@ -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));
|
||||
|
255
core/embed/rust/src/ui/model_tr/component/flow.rs
Normal file
@ -0,0 +1,255 @@
|
||||
use crate::{
|
||||
micropython::buffer::StrBuffer,
|
||||
ui::{
|
||||
component::{Child, Component, Event, EventCtx, Pad},
|
||||
geometry::{Point, Rect},
|
||||
},
|
||||
};
|
||||
|
||||
use super::{
|
||||
common, theme, ButtonAction, ButtonController, ButtonControllerMsg, ButtonLayout, ButtonPos,
|
||||
FlowPages, Page,
|
||||
};
|
||||
|
||||
/// To be returned directly from Flow.
|
||||
pub enum FlowMsg {
|
||||
Confirmed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
// TODO: consider each FlowPage having the ability
|
||||
// to handle custom actions triggered by some btn.
|
||||
|
||||
pub struct Flow<F, const M: usize> {
|
||||
pages: FlowPages<F, M>,
|
||||
current_page: Page<M>,
|
||||
common_title: Option<StrBuffer>,
|
||||
content_area: Rect,
|
||||
pad: Pad,
|
||||
buttons: Child<ButtonController<&'static str>>,
|
||||
page_counter: u8,
|
||||
}
|
||||
|
||||
impl<F, const M: usize> Flow<F, M>
|
||||
where
|
||||
F: Fn(u8) -> Page<M>,
|
||||
{
|
||||
pub fn new(pages: FlowPages<F, M>) -> Self {
|
||||
let current_page = pages.get(0);
|
||||
Self {
|
||||
pages,
|
||||
current_page,
|
||||
common_title: None,
|
||||
content_area: Rect::zero(),
|
||||
pad: Pad::with_background(theme::BG),
|
||||
// Setting empty layout for now, we do not yet know how many sub-pages the first page
|
||||
// has. Initial button layout will be set in `place()` after we can call
|
||||
// `content.page_count()`.
|
||||
buttons: Child::new(ButtonController::new(ButtonLayout::empty())),
|
||||
page_counter: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Adding a common title to all pages. The title will not be colliding
|
||||
/// with the page content, as the content will be offset.
|
||||
pub fn with_common_title(mut self, title: StrBuffer) -> Self {
|
||||
self.common_title = Some(title);
|
||||
self
|
||||
}
|
||||
|
||||
/// Placing current page, setting current buttons and clearing.
|
||||
fn update(&mut self, ctx: &mut EventCtx, get_new_page: bool) {
|
||||
if get_new_page {
|
||||
self.current_page = self.pages.get(self.page_counter);
|
||||
}
|
||||
let content_area = self.content_area;
|
||||
self.current_page.place(content_area);
|
||||
self.set_buttons(ctx);
|
||||
self.clear(ctx);
|
||||
}
|
||||
|
||||
/// Clearing the whole area and requesting repaint.
|
||||
fn clear(&mut self, ctx: &mut EventCtx) {
|
||||
self.pad.clear();
|
||||
ctx.request_paint();
|
||||
}
|
||||
|
||||
/// Going to the previous page.
|
||||
fn go_to_prev_page(&mut self, ctx: &mut EventCtx) {
|
||||
self.page_counter -= 1;
|
||||
self.update(ctx, true);
|
||||
}
|
||||
|
||||
/// Going to the next page.
|
||||
fn go_to_next_page(&mut self, ctx: &mut EventCtx) {
|
||||
self.page_counter += 1;
|
||||
self.update(ctx, true);
|
||||
}
|
||||
|
||||
/// Going to page by its absolute index.
|
||||
/// Negative index means counting from the end.
|
||||
fn go_to_page_absolute(&mut self, index: i16, ctx: &mut EventCtx) {
|
||||
if index < 0 {
|
||||
self.page_counter = (self.pages.count() as i16 + index) as u8;
|
||||
} else {
|
||||
self.page_counter = index as u8;
|
||||
}
|
||||
self.update(ctx, true);
|
||||
}
|
||||
|
||||
/// Jumping to another page relative to the current one.
|
||||
fn go_to_page_relative(&mut self, jump: i16, ctx: &mut EventCtx) {
|
||||
self.page_counter = (self.page_counter as i16 + jump) as u8;
|
||||
self.update(ctx, true);
|
||||
}
|
||||
|
||||
/// Updating the visual state of the buttons after each event.
|
||||
/// All three buttons are handled based upon the current choice.
|
||||
/// If defined in the current choice, setting their text,
|
||||
/// whether they are long-pressed, and painting them.
|
||||
///
|
||||
/// NOTE: ButtonController is handling the painting, and
|
||||
/// it will not repaint the buttons unless some of them changed.
|
||||
fn set_buttons(&mut self, ctx: &mut EventCtx) {
|
||||
let btn_layout = self.current_page.btn_layout();
|
||||
self.buttons.mutate(ctx, |_ctx, buttons| {
|
||||
buttons.set(btn_layout);
|
||||
});
|
||||
}
|
||||
|
||||
/// When current choice contains paginated content, it may use the button
|
||||
/// event to just paginate itself.
|
||||
fn event_consumed_by_current_choice(&mut self, ctx: &mut EventCtx, pos: ButtonPos) -> bool {
|
||||
if matches!(pos, ButtonPos::Left) && self.current_page.has_prev_page() {
|
||||
self.current_page.go_to_prev_page();
|
||||
self.update(ctx, false);
|
||||
true
|
||||
} else if matches!(pos, ButtonPos::Right) && self.current_page.has_next_page() {
|
||||
self.current_page.go_to_next_page();
|
||||
self.update(ctx, false);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, const M: usize> Component for Flow<F, M>
|
||||
where
|
||||
F: Fn(u8) -> Page<M>,
|
||||
{
|
||||
type Msg = FlowMsg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
let (title_content_area, button_area) = bounds.split_bottom(theme::BUTTON_HEIGHT);
|
||||
// Accounting for possible title
|
||||
let content_area = if self.common_title.is_some() {
|
||||
title_content_area.split_top(10).1
|
||||
} else {
|
||||
title_content_area
|
||||
};
|
||||
self.content_area = content_area;
|
||||
|
||||
// We finally found how long is the first page, and can set its button layout.
|
||||
self.current_page.place(content_area);
|
||||
self.buttons = Child::new(ButtonController::new(self.current_page.btn_layout()));
|
||||
|
||||
self.pad.place(title_content_area);
|
||||
self.buttons.place(button_area);
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
let button_event = self.buttons.event(ctx, event);
|
||||
|
||||
// Do something when a button was triggered
|
||||
// and we have some action connected with it
|
||||
if let Some(ButtonControllerMsg::Triggered(pos)) = button_event {
|
||||
// When there is a previous or next screen in the current flow,
|
||||
// handle that first and in case it triggers, then do not continue
|
||||
if self.event_consumed_by_current_choice(ctx, pos) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let actions = self.current_page.btn_actions();
|
||||
let action = actions.get_action(pos);
|
||||
if let Some(action) = action {
|
||||
match action {
|
||||
ButtonAction::PrevPage => {
|
||||
self.go_to_prev_page(ctx);
|
||||
return None;
|
||||
}
|
||||
ButtonAction::NextPage => {
|
||||
self.go_to_next_page(ctx);
|
||||
return None;
|
||||
}
|
||||
ButtonAction::GoToIndex(index) => {
|
||||
self.go_to_page_absolute(index, ctx);
|
||||
return None;
|
||||
}
|
||||
ButtonAction::MovePageRelative(jump) => {
|
||||
self.go_to_page_relative(jump, ctx);
|
||||
return None;
|
||||
}
|
||||
ButtonAction::Cancel => return Some(FlowMsg::Cancelled),
|
||||
ButtonAction::Confirm => return Some(FlowMsg::Confirmed),
|
||||
ButtonAction::Select => {}
|
||||
ButtonAction::Action(_) => {}
|
||||
}
|
||||
}
|
||||
};
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
// TODO: might put horizontal scrollbar at the top right
|
||||
self.pad.paint();
|
||||
self.buttons.paint();
|
||||
if let Some(title) = &self.common_title {
|
||||
common::paint_header(Point::zero(), title, None);
|
||||
}
|
||||
self.current_page.paint();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
use heapless::String;
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<F, const M: usize> crate::trace::Trace for Flow<F, M>
|
||||
where
|
||||
F: Fn(u8) -> Page<M>,
|
||||
{
|
||||
/// Accounting for the possibility that button is connected with the
|
||||
/// currently paginated flow_page (only Prev or Next in that case).
|
||||
fn get_btn_action(&self, pos: ButtonPos) -> String<25> {
|
||||
if matches!(pos, ButtonPos::Left) && self.current_page.has_prev_page() {
|
||||
ButtonAction::PrevPage.string()
|
||||
} else if matches!(pos, ButtonPos::Right) && self.current_page.has_next_page() {
|
||||
ButtonAction::NextPage.string()
|
||||
} else {
|
||||
let btn_actions = self.current_page.btn_actions();
|
||||
|
||||
match btn_actions.get_action(pos) {
|
||||
Some(action) => action.string(),
|
||||
None => ButtonAction::empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("Flow");
|
||||
t.kw_pair("flow_page", inttostr!(self.page_counter));
|
||||
t.kw_pair("flow_page_count", inttostr!(self.pages.count()));
|
||||
|
||||
self.report_btn_actions(t);
|
||||
|
||||
if let Some(title) = &self.common_title {
|
||||
t.title(title.as_ref());
|
||||
}
|
||||
t.field("content_area", &self.content_area);
|
||||
t.field("buttons", &self.buttons);
|
||||
t.field("flow_page", &self.current_page);
|
||||
t.close()
|
||||
}
|
||||
}
|
309
core/embed/rust/src/ui/model_tr/component/flow_pages.rs
Normal file
@ -0,0 +1,309 @@
|
||||
use crate::{
|
||||
micropython::{buffer::StrBuffer},
|
||||
ui::{
|
||||
component::Paginate,
|
||||
display::{Font, Icon, IconAndName},
|
||||
geometry::{Offset, Rect},
|
||||
model_tr::theme,
|
||||
util::ResultExt
|
||||
},
|
||||
};
|
||||
|
||||
use heapless::Vec;
|
||||
|
||||
use super::{
|
||||
flow_pages_poc_helpers::{
|
||||
LayoutFit, LayoutSink, LineAlignment, Op, TextLayout, TextNoOp, TextRenderer, TextStyle,
|
||||
ToDisplay,
|
||||
},
|
||||
ButtonActions, ButtonDetails, ButtonLayout,
|
||||
};
|
||||
|
||||
/// Holding specific workflows that are created in `layout.rs`.
|
||||
/// Is returning a `Page` (page/screen) on demand
|
||||
/// based on the current page in `Flow`.
|
||||
/// Before, when `layout.rs` was defining a `heapless::Vec` of `Page`s,
|
||||
/// it was a very stack-expensive operation and StackOverflow was encountered.
|
||||
/// With this "lazy-loading" approach (creating each page on demand) we can
|
||||
/// have theoretically unlimited number of pages without triggering SO.
|
||||
/// (Currently only the current page is stored on stack - in
|
||||
/// `Flow::current_page`.)
|
||||
pub struct FlowPages<F, const M: usize> {
|
||||
/// Function/closure that will return appropriate page on demand.
|
||||
get_page: F,
|
||||
/// Number of pages in the flow.
|
||||
page_count: u8,
|
||||
}
|
||||
|
||||
impl<F, const M: usize> FlowPages<F, M>
|
||||
where
|
||||
F: Fn(u8) -> Page<M>,
|
||||
{
|
||||
pub fn new(get_page: F, page_count: u8) -> Self {
|
||||
Self {
|
||||
get_page,
|
||||
page_count,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, page_index: u8) -> Page<M> {
|
||||
(self.get_page)(page_index)
|
||||
}
|
||||
|
||||
pub fn count(&self) -> u8 {
|
||||
self.page_count
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Page<const M: usize> {
|
||||
ops: Vec<Op, M>,
|
||||
layout: TextLayout,
|
||||
btn_layout: ButtonLayout<&'static str>,
|
||||
btn_actions: ButtonActions,
|
||||
current_page: usize,
|
||||
page_count: usize,
|
||||
char_offset: usize,
|
||||
}
|
||||
|
||||
// For `layout.rs`
|
||||
impl<const M: usize> Page<M> {
|
||||
pub fn new(btn_layout: ButtonLayout<&'static str>, btn_actions: ButtonActions) -> Self {
|
||||
let style = TextStyle::new(
|
||||
Font::NORMAL,
|
||||
theme::FG,
|
||||
theme::BG,
|
||||
theme::FG,
|
||||
theme::FG,
|
||||
);
|
||||
Self {
|
||||
ops: Vec::new(),
|
||||
layout: TextLayout::new(style),
|
||||
btn_layout,
|
||||
btn_actions,
|
||||
current_page: 0,
|
||||
page_count: 1,
|
||||
char_offset: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For `flow.rs`
|
||||
impl<const M: usize> Page<M> {
|
||||
pub fn paint(&mut self) {
|
||||
self.change_page(self.current_page);
|
||||
self.layout_content(&mut TextRenderer);
|
||||
}
|
||||
|
||||
pub fn btn_layout(&self) -> ButtonLayout<&'static str> {
|
||||
// When we are in pagination inside this flow,
|
||||
// show the up and down arrows on appropriate sides
|
||||
let current = self.btn_layout.clone();
|
||||
|
||||
let btn_left = if self.has_prev_page() {
|
||||
Some(ButtonDetails::up_arrow_icon_wide())
|
||||
} else {
|
||||
current.btn_left
|
||||
};
|
||||
let btn_right = if self.has_next_page() {
|
||||
Some(ButtonDetails::down_arrow_icon_wide())
|
||||
} else {
|
||||
current.btn_right
|
||||
};
|
||||
|
||||
ButtonLayout::new(btn_left, current.btn_middle, btn_right)
|
||||
}
|
||||
|
||||
pub fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.layout.bounds = bounds;
|
||||
self.page_count = self.page_count();
|
||||
bounds
|
||||
}
|
||||
|
||||
pub fn btn_actions(&self) -> ButtonActions {
|
||||
self.btn_actions.clone()
|
||||
}
|
||||
|
||||
pub fn has_prev_page(&self) -> bool {
|
||||
self.current_page > 0
|
||||
}
|
||||
|
||||
pub fn has_next_page(&self) -> bool {
|
||||
self.current_page < self.page_count - 1
|
||||
}
|
||||
|
||||
pub fn go_to_prev_page(&mut self) {
|
||||
self.current_page -= 1;
|
||||
}
|
||||
|
||||
pub fn go_to_next_page(&mut self) {
|
||||
self.current_page += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// For `layout.rs` - single operations
|
||||
impl<const M: usize> Page<M> {
|
||||
pub fn with_new_item(mut self, item: Op) -> Self {
|
||||
self.ops
|
||||
.push(item)
|
||||
.assert_if_debugging_ui("Could not push to self.ops");
|
||||
self
|
||||
}
|
||||
|
||||
pub fn text(self, text: StrBuffer) -> Self {
|
||||
self.with_new_item(Op::Text(ToDisplay::new(text)))
|
||||
}
|
||||
|
||||
pub fn newline(self) -> Self {
|
||||
self.with_new_item(Op::Text(ToDisplay::new("\n".into())))
|
||||
}
|
||||
|
||||
pub fn newline_half(self) -> Self {
|
||||
self.with_new_item(Op::Text(ToDisplay::new("\r".into())))
|
||||
}
|
||||
|
||||
pub fn next_page(self) -> Self {
|
||||
self.with_new_item(Op::NextPage)
|
||||
}
|
||||
|
||||
pub fn icon(self, icon: IconAndName) -> Self {
|
||||
self.with_new_item(Op::Icon(Icon::new(icon)))
|
||||
}
|
||||
|
||||
pub fn font(self, font: Font) -> Self {
|
||||
self.with_new_item(Op::Font(font))
|
||||
}
|
||||
|
||||
pub fn offset(self, offset: Offset) -> Self {
|
||||
self.with_new_item(Op::CursorOffset(offset))
|
||||
}
|
||||
|
||||
pub fn alignment(self, alignment: LineAlignment) -> Self {
|
||||
self.with_new_item(Op::LineAlignment(alignment))
|
||||
}
|
||||
}
|
||||
|
||||
// For `layout.rs` - aggregating operations
|
||||
impl<const M: usize> Page<M> {
|
||||
pub fn icon_label_text(self, icon: IconAndName, label: StrBuffer, text: StrBuffer) -> Self {
|
||||
self.icon_with_offset(icon, 3)
|
||||
.text_normal(label)
|
||||
.newline()
|
||||
.text_bold(text)
|
||||
}
|
||||
|
||||
pub fn icon_with_offset(self, icon: IconAndName, x_offset: i16) -> Self {
|
||||
self.icon(icon).offset(Offset::x(x_offset))
|
||||
}
|
||||
|
||||
pub fn text_normal(self, text: StrBuffer) -> Self {
|
||||
self.font(Font::NORMAL).text(text)
|
||||
}
|
||||
|
||||
pub fn text_bold(self, text: StrBuffer) -> Self {
|
||||
self.font(Font::BOLD).text(text)
|
||||
}
|
||||
}
|
||||
|
||||
// For painting and pagination
|
||||
impl<const M: usize> Page<M> {
|
||||
pub fn set_char_offset(&mut self, char_offset: usize) {
|
||||
self.char_offset = char_offset;
|
||||
}
|
||||
|
||||
pub fn layout_content(&self, sink: &mut dyn LayoutSink) -> LayoutFit {
|
||||
let mut cursor = self.layout.initial_cursor();
|
||||
self.layout
|
||||
.layout_ops(self.ops.clone(), &mut cursor, self.char_offset, sink)
|
||||
}
|
||||
}
|
||||
|
||||
// Pagination
|
||||
impl<const M: usize> Paginate for Page<M> {
|
||||
fn page_count(&mut self) -> usize {
|
||||
let mut page_count = 1; // There's always at least one page.
|
||||
let mut char_offset = 0;
|
||||
|
||||
// Make sure we're starting from the beginning.
|
||||
self.set_char_offset(char_offset);
|
||||
|
||||
// Looping through the content and counting pages
|
||||
// until we finally fit.
|
||||
loop {
|
||||
let fit = self.layout_content(&mut TextNoOp);
|
||||
match fit {
|
||||
LayoutFit::Fitting { .. } => {
|
||||
break; // TODO: We should consider if there's more content
|
||||
// to render.
|
||||
}
|
||||
LayoutFit::OutOfBounds {
|
||||
processed_chars, ..
|
||||
} => {
|
||||
page_count += 1;
|
||||
char_offset += processed_chars;
|
||||
self.set_char_offset(char_offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset the char offset back to the beginning.
|
||||
self.set_char_offset(0);
|
||||
|
||||
page_count
|
||||
}
|
||||
|
||||
fn change_page(&mut self, to_page: usize) {
|
||||
let mut active_page = 0;
|
||||
let mut char_offset = 0;
|
||||
|
||||
// Make sure we're starting from the beginning.
|
||||
self.set_char_offset(char_offset);
|
||||
|
||||
// Looping through the content until we arrive at
|
||||
// the wanted page.
|
||||
while active_page < to_page {
|
||||
let fit = self.layout_content(&mut TextNoOp);
|
||||
match fit {
|
||||
LayoutFit::Fitting { .. } => {
|
||||
break; // TODO: We should consider if there's more content
|
||||
// to render.
|
||||
}
|
||||
LayoutFit::OutOfBounds {
|
||||
processed_chars, ..
|
||||
} => {
|
||||
active_page += 1;
|
||||
char_offset += processed_chars;
|
||||
self.set_char_offset(char_offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
pub mod trace {
|
||||
use crate::ui::model_tr::component::flow_pages_poc_helpers::TraceSink;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub struct TraceText<'a, const M: usize>(pub &'a Page<M>);
|
||||
|
||||
impl<'a, const M: usize> crate::trace::Trace for TraceText<'a, M> {
|
||||
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
|
||||
d.content_flag();
|
||||
self.0.layout_content(&mut TraceSink(d));
|
||||
d.content_flag();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<const M: usize> crate::trace::Trace for Page<M> {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("Page");
|
||||
t.kw_pair("active_page", inttostr!(self.current_page as u8));
|
||||
t.kw_pair("page_count", inttostr!(self.page_count as u8));
|
||||
t.field("content", &trace::TraceText(self));
|
||||
t.close();
|
||||
}
|
||||
}
|
@ -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<T>
|
||||
impl<T> ButtonPage<&'static str, T>
|
||||
where
|
||||
T: Paginate,
|
||||
T: Component,
|
||||
{
|
||||
pub fn new(content: T, background: Color) -> Self {
|
||||
/// Constructor for `&'static str` button-text type.
|
||||
pub fn new_str(content: T, background: Color) -> Self {
|
||||
Self {
|
||||
content,
|
||||
scrollbar: ScrollBar::vertical(),
|
||||
content: Child::new(content),
|
||||
scrollbar: Child::new(ScrollBar::vertical_to_be_filled_later()),
|
||||
pad: Pad::with_background(background),
|
||||
prev: Button::with_text(ButtonPos::Left, "BACK", theme::button_cancel()),
|
||||
next: Button::with_text(ButtonPos::Right, "NEXT", theme::button_default()),
|
||||
cancel: Button::with_text(ButtonPos::Left, "CANCEL", theme::button_cancel()),
|
||||
confirm: Button::with_text(ButtonPos::Right, "CONFIRM", theme::button_default()),
|
||||
cancel_btn_details: Some(ButtonDetails::cancel_icon()),
|
||||
confirm_btn_details: Some(ButtonDetails::text("CONFIRM")),
|
||||
back_btn_details: Some(ButtonDetails::up_arrow_icon_wide()),
|
||||
next_btn_details: Some(ButtonDetails::down_arrow_icon_wide()),
|
||||
// Setting empty layout for now, we do not yet know the page count.
|
||||
// Initial button layout will be set in `place()` after we can call
|
||||
// `content.page_count()`.
|
||||
buttons: Child::new(ButtonController::new(ButtonLayout::empty())),
|
||||
}
|
||||
}
|
||||
|
||||
fn change_page(&mut self, ctx: &mut EventCtx, page: usize) {
|
||||
// Change the page in the content, clear the background under it and make sure
|
||||
// it gets completely repainted.
|
||||
self.content.change_page(page);
|
||||
self.content.request_complete_repaint(ctx);
|
||||
self.pad.clear();
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Component for ButtonPage<T>
|
||||
impl<T> ButtonPage<StrBuffer, T>
|
||||
where
|
||||
T: Paginate,
|
||||
T: Component,
|
||||
{
|
||||
/// Constructor for `StrBuffer` button-text type.
|
||||
pub fn new_str_buf(content: T, background: Color) -> Self {
|
||||
Self {
|
||||
content: Child::new(content),
|
||||
scrollbar: Child::new(ScrollBar::vertical_to_be_filled_later()),
|
||||
pad: Pad::with_background(background),
|
||||
cancel_btn_details: Some(ButtonDetails::cancel_icon()),
|
||||
confirm_btn_details: Some(ButtonDetails::text("CONFIRM".into())),
|
||||
back_btn_details: Some(ButtonDetails::up_arrow_icon_wide()),
|
||||
next_btn_details: Some(ButtonDetails::down_arrow_icon_wide()),
|
||||
// Setting empty layout for now, we do not yet know the page count.
|
||||
// Initial button layout will be set in `place()` after we can call
|
||||
// `content.page_count()`.
|
||||
buttons: Child::new(ButtonController::new(ButtonLayout::empty())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, T> ButtonPage<S, T>
|
||||
where
|
||||
T: Paginate,
|
||||
T: Component,
|
||||
S: AsRef<str>,
|
||||
S: Clone,
|
||||
{
|
||||
pub fn with_cancel_btn(mut self, btn_details: Option<ButtonDetails<S>>) -> Self {
|
||||
self.cancel_btn_details = btn_details;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_confirm_btn(mut self, btn_details: Option<ButtonDetails<S>>) -> Self {
|
||||
self.confirm_btn_details = btn_details;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_back_btn(mut self, btn_details: Option<ButtonDetails<S>>) -> Self {
|
||||
self.back_btn_details = btn_details;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_next_btn(mut self, btn_details: Option<ButtonDetails<S>>) -> Self {
|
||||
self.next_btn_details = btn_details;
|
||||
self
|
||||
}
|
||||
|
||||
/// Basically just determining whether the right button for
|
||||
/// initial page should be "NEXT" or "CONFIRM".
|
||||
/// Can only be called when we know the final page_count.
|
||||
fn set_buttons_for_initial_page(&mut self, page_count: usize) {
|
||||
let btn_layout = self.get_button_layout(false, page_count > 1);
|
||||
self.buttons = Child::new(ButtonController::new(btn_layout));
|
||||
}
|
||||
|
||||
/// Called when user pressed "BACK" or "NEXT".
|
||||
/// Change the page in the content, clear the background under it and make
|
||||
/// sure it gets completely repainted. Also updating the buttons.
|
||||
fn change_page(&mut self, ctx: &mut EventCtx) {
|
||||
let active_page = self.scrollbar.inner().active_page;
|
||||
self.content.inner_mut().change_page(active_page);
|
||||
self.content.request_complete_repaint(ctx);
|
||||
self.scrollbar.request_complete_repaint(ctx);
|
||||
self.update_buttons(ctx);
|
||||
self.pad.clear();
|
||||
}
|
||||
|
||||
/// Reflecting the current page in the buttons.
|
||||
fn update_buttons(&mut self, ctx: &mut EventCtx) {
|
||||
let btn_layout = self.get_button_layout(
|
||||
self.scrollbar.inner().has_previous_page(),
|
||||
self.scrollbar.inner().has_next_page(),
|
||||
);
|
||||
self.buttons.mutate(ctx, |_ctx, buttons| {
|
||||
buttons.set(btn_layout);
|
||||
});
|
||||
}
|
||||
|
||||
fn get_button_layout(&self, has_prev: bool, has_next: bool) -> ButtonLayout<S> {
|
||||
let btn_left = self.get_left_button_details(has_prev);
|
||||
let btn_right = self.get_right_button_details(has_next);
|
||||
ButtonLayout::new(btn_left, None, btn_right)
|
||||
}
|
||||
|
||||
fn get_left_button_details(&self, has_prev_page: bool) -> Option<ButtonDetails<S>> {
|
||||
if has_prev_page {
|
||||
self.back_btn_details.clone()
|
||||
} else {
|
||||
self.cancel_btn_details.clone()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_right_button_details(&self, has_next_page: bool) -> Option<ButtonDetails<S>> {
|
||||
if has_next_page {
|
||||
self.next_btn_details.clone()
|
||||
} else {
|
||||
self.confirm_btn_details.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, T> Component for ButtonPage<S, T>
|
||||
where
|
||||
S: Clone,
|
||||
S: AsRef<str>,
|
||||
T: Component,
|
||||
T: Paginate,
|
||||
{
|
||||
type Msg = PageMsg<T::Msg, bool>;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
let button_height = Font::BOLD.line_height() + 2;
|
||||
let (content_area, button_area) = bounds.split_bottom(button_height);
|
||||
let (content_area, scrollbar_area) = content_area.split_right(ScrollBar::WIDTH);
|
||||
let (content_and_scrollbar_area, button_area) = bounds.split_bottom(theme::BUTTON_HEIGHT);
|
||||
let (content_area, scrollbar_area) =
|
||||
content_and_scrollbar_area.split_right(ScrollBar::WIDTH);
|
||||
let content_area = content_area.inset(Insets::top(1));
|
||||
self.pad.place(bounds);
|
||||
// Do not pad the button area nor the scrollbar, leave it to them
|
||||
self.pad.place(content_area);
|
||||
self.content.place(content_area);
|
||||
let page_count = self.content.page_count();
|
||||
self.scrollbar.set_count_and_active_page(page_count, 0);
|
||||
// Need to be called here, only after content is placed
|
||||
// and we can calculate the page count
|
||||
let page_count = self.content.inner_mut().page_count();
|
||||
self.scrollbar.inner_mut().set_page_count(page_count);
|
||||
self.scrollbar.place(scrollbar_area);
|
||||
self.prev.place(button_area);
|
||||
self.next.place(button_area);
|
||||
self.cancel.place(button_area);
|
||||
self.confirm.place(button_area);
|
||||
self.set_buttons_for_initial_page(page_count);
|
||||
self.buttons.place(button_area);
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
ctx.set_page_count(self.scrollbar.page_count);
|
||||
if self.scrollbar.has_previous_page() {
|
||||
if let Some(ButtonMsg::Clicked) = self.prev.event(ctx, event) {
|
||||
// Scroll up.
|
||||
self.scrollbar.go_to_previous_page();
|
||||
self.change_page(ctx, self.scrollbar.active_page);
|
||||
return None;
|
||||
}
|
||||
} else if let Some(ButtonMsg::Clicked) = self.cancel.event(ctx, event) {
|
||||
return Some(PageMsg::Controls(false));
|
||||
}
|
||||
ctx.set_page_count(self.scrollbar.inner().page_count);
|
||||
let button_event = self.buttons.event(ctx, event);
|
||||
|
||||
if self.scrollbar.has_next_page() {
|
||||
if let Some(ButtonMsg::Clicked) = self.next.event(ctx, event) {
|
||||
// Scroll down.
|
||||
self.scrollbar.go_to_next_page();
|
||||
self.change_page(ctx, self.scrollbar.active_page);
|
||||
return None;
|
||||
if let Some(ButtonControllerMsg::Triggered(pos)) = button_event {
|
||||
match pos {
|
||||
ButtonPos::Left => {
|
||||
if self.scrollbar.inner().has_previous_page() {
|
||||
// Clicked BACK. Scroll up.
|
||||
self.scrollbar.inner_mut().go_to_previous_page();
|
||||
self.change_page(ctx);
|
||||
} else {
|
||||
// Clicked CANCEL. Send result.
|
||||
return Some(PageMsg::Controls(false));
|
||||
}
|
||||
}
|
||||
ButtonPos::Right => {
|
||||
if self.scrollbar.inner().has_next_page() {
|
||||
// Clicked NEXT. Scroll down.
|
||||
self.scrollbar.inner_mut().go_to_next_page();
|
||||
self.change_page(ctx);
|
||||
} else {
|
||||
// Clicked CONFIRM. Send result.
|
||||
return Some(PageMsg::Controls(true));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else if let Some(ButtonMsg::Clicked) = self.confirm.event(ctx, event) {
|
||||
return Some(PageMsg::Controls(true));
|
||||
}
|
||||
|
||||
if let Some(msg) = self.content.event(ctx, event) {
|
||||
@ -100,128 +216,60 @@ where
|
||||
self.pad.paint();
|
||||
self.content.paint();
|
||||
self.scrollbar.paint();
|
||||
if self.scrollbar.has_previous_page() {
|
||||
self.prev.paint();
|
||||
} else {
|
||||
self.cancel.paint();
|
||||
}
|
||||
if self.scrollbar.has_next_page() {
|
||||
self.next.paint();
|
||||
} else {
|
||||
self.confirm.paint();
|
||||
}
|
||||
self.buttons.paint();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T> crate::trace::Trace for ButtonPage<T>
|
||||
use super::ButtonAction;
|
||||
#[cfg(feature = "ui_debug")]
|
||||
use heapless::String;
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<S, T> crate::trace::Trace for ButtonPage<S, T>
|
||||
where
|
||||
T: crate::trace::Trace,
|
||||
S: AsRef<str>,
|
||||
{
|
||||
fn get_btn_action(&self, pos: ButtonPos) -> String<25> {
|
||||
match pos {
|
||||
ButtonPos::Left => {
|
||||
if self.scrollbar.inner().has_previous_page() {
|
||||
ButtonAction::PrevPage.string()
|
||||
} else if self.cancel_btn_details.is_some() {
|
||||
ButtonAction::Cancel.string()
|
||||
} else {
|
||||
ButtonAction::empty()
|
||||
}
|
||||
}
|
||||
ButtonPos::Right => {
|
||||
if self.scrollbar.inner().has_next_page() {
|
||||
ButtonAction::NextPage.string()
|
||||
} else if self.confirm_btn_details.is_some() {
|
||||
ButtonAction::Confirm.string()
|
||||
} else {
|
||||
ButtonAction::empty()
|
||||
}
|
||||
}
|
||||
ButtonPos::Middle => ButtonAction::empty(),
|
||||
}
|
||||
}
|
||||
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("ButtonPage");
|
||||
t.field("active_page", &self.scrollbar.active_page);
|
||||
t.field("page_count", &self.scrollbar.page_count);
|
||||
t.kw_pair(
|
||||
"active_page",
|
||||
inttostr!(self.scrollbar.inner().active_page as u8),
|
||||
);
|
||||
t.kw_pair(
|
||||
"page_count",
|
||||
inttostr!(self.scrollbar.inner().page_count as u8),
|
||||
);
|
||||
self.report_btn_actions(t);
|
||||
// TODO: it seems the button text is not updated when paginating (but actions
|
||||
// above are)
|
||||
t.field("buttons", &self.buttons);
|
||||
t.field("content", &self.content);
|
||||
t.close();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ScrollBar {
|
||||
area: Rect,
|
||||
page_count: usize,
|
||||
active_page: usize,
|
||||
}
|
||||
|
||||
impl ScrollBar {
|
||||
pub const WIDTH: i16 = 8;
|
||||
pub const DOT_SIZE: Offset = Offset::new(4, 4);
|
||||
pub const DOT_INTERVAL: i16 = 6;
|
||||
|
||||
pub fn vertical() -> Self {
|
||||
Self {
|
||||
area: Rect::zero(),
|
||||
page_count: 0,
|
||||
active_page: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_count_and_active_page(&mut self, page_count: usize, active_page: usize) {
|
||||
self.page_count = page_count;
|
||||
self.active_page = active_page;
|
||||
}
|
||||
|
||||
pub fn has_next_page(&self) -> bool {
|
||||
self.active_page < self.page_count - 1
|
||||
}
|
||||
|
||||
pub fn has_previous_page(&self) -> bool {
|
||||
self.active_page > 0
|
||||
}
|
||||
|
||||
pub fn go_to_next_page(&mut self) {
|
||||
self.active_page = self.active_page.saturating_add(1).min(self.page_count - 1);
|
||||
}
|
||||
|
||||
pub fn go_to_previous_page(&mut self) {
|
||||
self.active_page = self.active_page.saturating_sub(1);
|
||||
}
|
||||
|
||||
fn paint_dot(&self, active: bool, top_left: Point) {
|
||||
let sides = [
|
||||
Rect::from_top_left_and_size(top_left + Offset::x(1), Offset::new(2, 1)),
|
||||
Rect::from_top_left_and_size(top_left + Offset::y(1), Offset::new(1, 2)),
|
||||
Rect::from_top_left_and_size(
|
||||
top_left + Offset::new(1, Self::DOT_SIZE.y - 1),
|
||||
Offset::new(2, 1),
|
||||
),
|
||||
Rect::from_top_left_and_size(
|
||||
top_left + Offset::new(Self::DOT_SIZE.x - 1, 1),
|
||||
Offset::new(1, 2),
|
||||
),
|
||||
];
|
||||
for side in sides {
|
||||
display::rect_fill(side, theme::FG)
|
||||
}
|
||||
if active {
|
||||
display::rect_fill(
|
||||
Rect::from_top_left_and_size(top_left, Self::DOT_SIZE).inset(Insets::uniform(1)),
|
||||
theme::FG,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for ScrollBar {
|
||||
type Msg = Never;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.area = bounds;
|
||||
self.area
|
||||
}
|
||||
|
||||
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
let count = self.page_count as i16;
|
||||
let interval = {
|
||||
let available_height = self.area.height();
|
||||
let naive_height = count * Self::DOT_INTERVAL;
|
||||
if naive_height > available_height {
|
||||
available_height / count
|
||||
} else {
|
||||
Self::DOT_INTERVAL
|
||||
}
|
||||
};
|
||||
let mut dot = Point::new(
|
||||
self.area.center().x - Self::DOT_SIZE.x / 2,
|
||||
self.area.center().y - (count / 2) * interval,
|
||||
);
|
||||
for i in 0..self.page_count {
|
||||
self.paint_dot(i == self.active_page, dot);
|
||||
dot.y += interval
|
||||
}
|
||||
}
|
||||
}
|
||||
|
365
core/embed/rust/src/ui/model_tr/component/passphrase.rs
Normal file
@ -0,0 +1,365 @@
|
||||
use crate::{
|
||||
time::Duration,
|
||||
ui::{
|
||||
component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx},
|
||||
geometry::Rect,
|
||||
},
|
||||
};
|
||||
|
||||
use super::{
|
||||
choice_item::BigCharacterChoiceItem, ButtonDetails, ButtonLayout, ChangingTextLine,
|
||||
ChoiceFactory, ChoiceItem, ChoicePage, ChoicePageMsg, MultilineTextChoiceItem, TextChoiceItem,
|
||||
};
|
||||
use heapless::String;
|
||||
|
||||
pub enum PassphraseEntryMsg {
|
||||
Confirmed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
/// Defines the choices currently available on the screen
|
||||
#[derive(PartialEq, Clone)]
|
||||
enum ChoiceCategory {
|
||||
Menu,
|
||||
LowercaseLetter,
|
||||
UppercaseLetter,
|
||||
Digit,
|
||||
SpecialSymbol,
|
||||
}
|
||||
|
||||
const MAX_PASSPHRASE_LENGTH: usize = 50;
|
||||
const HOLD_DURATION: Duration = Duration::from_secs(1);
|
||||
|
||||
const DIGITS: [char; 10] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
||||
const LOWERCASE_LETTERS: [char; 26] = [
|
||||
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's',
|
||||
't', 'u', 'v', 'w', 'x', 'y', 'z',
|
||||
];
|
||||
const UPPERCASE_LETTERS: [char; 26] = [
|
||||
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
|
||||
'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
|
||||
];
|
||||
const SPECIAL_SYMBOLS: [char; 30] = [
|
||||
'_', '<', '>', '.', ':', '@', '/', '|', '\\', '!', '(', ')', '+', '%', '&', '-', '[', ']', '?',
|
||||
'{', '}', ',', '\'', '`', ';', '"', '~', '$', '^', '=',
|
||||
];
|
||||
const MENU_LENGTH: usize = 6;
|
||||
const DEL_INDEX: usize = MENU_LENGTH - 1;
|
||||
const SHOW_INDEX: usize = MENU_LENGTH - 2;
|
||||
const MENU: [&str; MENU_LENGTH] = ["abc", "ABC", "123", "*#_", "SHOW PASS", "DEL LAST CHAR"];
|
||||
|
||||
/// Get a character at a specified index for a specified category.
|
||||
fn get_char(current_category: &ChoiceCategory, index: u8) -> char {
|
||||
let index = index as usize;
|
||||
match current_category {
|
||||
ChoiceCategory::LowercaseLetter => LOWERCASE_LETTERS[index],
|
||||
ChoiceCategory::UppercaseLetter => UPPERCASE_LETTERS[index],
|
||||
ChoiceCategory::Digit => DIGITS[index],
|
||||
ChoiceCategory::SpecialSymbol => SPECIAL_SYMBOLS[index],
|
||||
ChoiceCategory::Menu => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return category from menu based on page index.
|
||||
fn get_category_from_menu(page_index: u8) -> ChoiceCategory {
|
||||
match page_index {
|
||||
0 => ChoiceCategory::LowercaseLetter,
|
||||
1 => ChoiceCategory::UppercaseLetter,
|
||||
2 => ChoiceCategory::Digit,
|
||||
3 => ChoiceCategory::SpecialSymbol,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// How many choices are available for a specified category.
|
||||
/// (does not count the extra MENU choice for characters)
|
||||
fn get_category_length(current_category: &ChoiceCategory) -> u8 {
|
||||
match current_category {
|
||||
ChoiceCategory::LowercaseLetter => LOWERCASE_LETTERS.len() as u8,
|
||||
ChoiceCategory::UppercaseLetter => UPPERCASE_LETTERS.len() as u8,
|
||||
ChoiceCategory::Digit => DIGITS.len() as u8,
|
||||
ChoiceCategory::SpecialSymbol => SPECIAL_SYMBOLS.len() as u8,
|
||||
ChoiceCategory::Menu => MENU.len() as u8,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this index is the MENU index - the last one in the list.
|
||||
fn is_menu_choice(current_category: &ChoiceCategory, page_index: u8) -> bool {
|
||||
if let ChoiceCategory::Menu = current_category {
|
||||
unreachable!()
|
||||
}
|
||||
let category_length = get_category_length(current_category);
|
||||
page_index == category_length
|
||||
}
|
||||
|
||||
struct ChoiceFactoryPassphrase {
|
||||
current_category: ChoiceCategory,
|
||||
}
|
||||
|
||||
impl ChoiceFactoryPassphrase {
|
||||
fn new(current_category: ChoiceCategory) -> Self {
|
||||
Self { current_category }
|
||||
}
|
||||
|
||||
/// MENU choices with accept and cancel hold-to-confirm side buttons.
|
||||
fn get_menu_item(&self, choice_index: u8) -> ChoiceItem {
|
||||
let choice = MENU[choice_index as usize];
|
||||
let item =
|
||||
MultilineTextChoiceItem::new(String::from(choice), ButtonLayout::default_three_icons())
|
||||
.use_delimiter(' ');
|
||||
let mut menu_item = ChoiceItem::MultilineText(item);
|
||||
|
||||
// Including accept button on the left and cancel on the very right
|
||||
// TODO: could have some icons instead of the shortcut text
|
||||
if choice_index == 0 {
|
||||
menu_item.set_left_btn(Some(
|
||||
ButtonDetails::text("ACC").with_duration(HOLD_DURATION),
|
||||
));
|
||||
} else if choice_index == MENU.len() as u8 - 1 {
|
||||
menu_item.set_right_btn(Some(
|
||||
ButtonDetails::text("CAN").with_duration(HOLD_DURATION),
|
||||
));
|
||||
}
|
||||
|
||||
menu_item
|
||||
}
|
||||
|
||||
/// Character choices with a MENU choice at the end (visible from start) to
|
||||
/// return back
|
||||
fn get_character_item(&self, choice_index: u8) -> ChoiceItem {
|
||||
if is_menu_choice(&self.current_category, choice_index) {
|
||||
let menu_choice =
|
||||
TextChoiceItem::new("MENU", ButtonLayout::three_icons_middle_text("RETURN"));
|
||||
ChoiceItem::Text(menu_choice)
|
||||
} else {
|
||||
let ch = get_char(&self.current_category, choice_index);
|
||||
let char_choice = BigCharacterChoiceItem::new(ch, ButtonLayout::default_three_icons());
|
||||
ChoiceItem::BigCharacter(char_choice)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ChoiceFactory for ChoiceFactoryPassphrase {
|
||||
fn get(&self, choice_index: u8) -> ChoiceItem {
|
||||
match self.current_category {
|
||||
ChoiceCategory::Menu => self.get_menu_item(choice_index),
|
||||
_ => self.get_character_item(choice_index),
|
||||
}
|
||||
}
|
||||
|
||||
fn count(&self) -> u8 {
|
||||
let length = get_category_length(&self.current_category);
|
||||
// All non-MENU categories have an extra item for returning back to MENU
|
||||
match self.current_category {
|
||||
ChoiceCategory::Menu => length,
|
||||
_ => length + 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Component for entering a passphrase.
|
||||
pub struct PassphraseEntry {
|
||||
choice_page: ChoicePage<ChoiceFactoryPassphrase>,
|
||||
passphrase_dots: Child<ChangingTextLine<String<MAX_PASSPHRASE_LENGTH>>>,
|
||||
show_plain_passphrase: bool,
|
||||
textbox: TextBox<MAX_PASSPHRASE_LENGTH>,
|
||||
current_category: ChoiceCategory,
|
||||
menu_position: u8, // position in the menu so we can return back
|
||||
}
|
||||
|
||||
impl PassphraseEntry {
|
||||
pub fn new() -> Self {
|
||||
let menu_choices = ChoiceFactoryPassphrase::new(ChoiceCategory::Menu);
|
||||
Self {
|
||||
choice_page: ChoicePage::new(menu_choices),
|
||||
passphrase_dots: Child::new(ChangingTextLine::center_mono(String::new())),
|
||||
show_plain_passphrase: false,
|
||||
textbox: TextBox::empty(),
|
||||
current_category: ChoiceCategory::Menu,
|
||||
menu_position: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_passphrase_dots(&mut self, ctx: &mut EventCtx) {
|
||||
// TODO: when the passphrase is longer than fits the screen, we might show
|
||||
// ellipsis
|
||||
if self.show_plain_passphrase {
|
||||
let passphrase = String::from(self.passphrase());
|
||||
self.passphrase_dots.inner_mut().update_text(passphrase);
|
||||
} else {
|
||||
let mut dots: String<MAX_PASSPHRASE_LENGTH> = String::new();
|
||||
for _ in 0..self.textbox.len() {
|
||||
unwrap!(dots.push_str("*"));
|
||||
}
|
||||
self.passphrase_dots.inner_mut().update_text(dots);
|
||||
}
|
||||
self.passphrase_dots.request_complete_repaint(ctx);
|
||||
}
|
||||
|
||||
fn append_char(&mut self, ctx: &mut EventCtx, ch: char) {
|
||||
self.textbox.append(ctx, ch);
|
||||
}
|
||||
|
||||
fn delete_last_digit(&mut self, ctx: &mut EventCtx) {
|
||||
self.textbox.delete_last(ctx);
|
||||
}
|
||||
|
||||
/// Displaying the MENU
|
||||
fn show_menu_page(&mut self, ctx: &mut EventCtx) {
|
||||
let menu_choices = ChoiceFactoryPassphrase::new(ChoiceCategory::Menu);
|
||||
self.choice_page.reset(ctx, menu_choices, true, false);
|
||||
// Going back to the last MENU position before showing the MENU
|
||||
self.choice_page.set_page_counter(ctx, self.menu_position);
|
||||
}
|
||||
|
||||
/// Displaying the character category
|
||||
fn show_category_page(&mut self, ctx: &mut EventCtx) {
|
||||
let category_choices = ChoiceFactoryPassphrase::new(self.current_category.clone());
|
||||
self.choice_page.reset(ctx, category_choices, true, true);
|
||||
}
|
||||
|
||||
pub fn passphrase(&self) -> &str {
|
||||
self.textbox.content()
|
||||
}
|
||||
|
||||
fn is_full(&self) -> bool {
|
||||
self.textbox.is_full()
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for PassphraseEntry {
|
||||
type Msg = PassphraseEntryMsg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
let passphrase_area_height = self.passphrase_dots.inner().needed_height();
|
||||
let (passphrase_area, choice_area) = bounds.split_top(passphrase_area_height);
|
||||
self.passphrase_dots.place(passphrase_area);
|
||||
self.choice_page.place(choice_area);
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
// Any event when showing real passphrase should hide it
|
||||
if self.show_plain_passphrase {
|
||||
self.show_plain_passphrase = false;
|
||||
self.update_passphrase_dots(ctx);
|
||||
}
|
||||
|
||||
let msg = self.choice_page.event(ctx, event);
|
||||
|
||||
if self.current_category == ChoiceCategory::Menu {
|
||||
match msg {
|
||||
// Going to new category, applying some action or returning the result
|
||||
Some(ChoicePageMsg::Choice(page_counter)) => match page_counter as usize {
|
||||
DEL_INDEX => {
|
||||
self.delete_last_digit(ctx);
|
||||
self.update_passphrase_dots(ctx);
|
||||
ctx.request_paint();
|
||||
}
|
||||
SHOW_INDEX => {
|
||||
self.show_plain_passphrase = true;
|
||||
self.update_passphrase_dots(ctx);
|
||||
ctx.request_paint();
|
||||
}
|
||||
_ => {
|
||||
self.menu_position = page_counter;
|
||||
self.current_category = get_category_from_menu(page_counter);
|
||||
self.show_category_page(ctx);
|
||||
ctx.request_paint();
|
||||
}
|
||||
},
|
||||
Some(ChoicePageMsg::LeftMost) => return Some(PassphraseEntryMsg::Confirmed),
|
||||
Some(ChoicePageMsg::RightMost) => return Some(PassphraseEntryMsg::Cancelled),
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
// Coming back to MENU or adding new character
|
||||
if let Some(ChoicePageMsg::Choice(page_counter)) = msg {
|
||||
if is_menu_choice(&self.current_category, page_counter) {
|
||||
self.current_category = ChoiceCategory::Menu;
|
||||
self.show_menu_page(ctx);
|
||||
ctx.request_paint();
|
||||
} else if !self.is_full() {
|
||||
let new_char = get_char(&self.current_category, page_counter);
|
||||
self.append_char(ctx, new_char);
|
||||
self.update_passphrase_dots(ctx);
|
||||
ctx.request_paint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.passphrase_dots.paint();
|
||||
self.choice_page.paint();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
use super::{ButtonAction, ButtonPos};
|
||||
#[cfg(feature = "ui_debug")]
|
||||
use crate::ui::util;
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for PassphraseEntry {
|
||||
fn get_btn_action(&self, pos: ButtonPos) -> String<25> {
|
||||
match pos {
|
||||
ButtonPos::Left => match self.current_category {
|
||||
ChoiceCategory::Menu => match self.choice_page.has_previous_choice() {
|
||||
true => ButtonAction::PrevPage.string(),
|
||||
false => ButtonAction::Confirm.string(),
|
||||
},
|
||||
_ => ButtonAction::PrevPage.string(),
|
||||
},
|
||||
ButtonPos::Right => match self.current_category {
|
||||
ChoiceCategory::Menu => match self.choice_page.has_next_choice() {
|
||||
true => ButtonAction::NextPage.string(),
|
||||
false => ButtonAction::Cancel.string(),
|
||||
},
|
||||
_ => ButtonAction::NextPage.string(),
|
||||
},
|
||||
ButtonPos::Middle => {
|
||||
let current_index = self.choice_page.page_index() as usize;
|
||||
match &self.current_category {
|
||||
ChoiceCategory::Menu => match current_index {
|
||||
DEL_INDEX => ButtonAction::Action("Del last char").string(),
|
||||
SHOW_INDEX => ButtonAction::Action("Show pass").string(),
|
||||
_ => ButtonAction::select_item(MENU[current_index]),
|
||||
},
|
||||
_ => {
|
||||
// There is "MENU" option at the end
|
||||
match self.choice_page.has_next_choice() {
|
||||
false => ButtonAction::Action("Back to MENU").string(),
|
||||
true => {
|
||||
let ch = match &self.current_category {
|
||||
ChoiceCategory::LowercaseLetter => {
|
||||
LOWERCASE_LETTERS[current_index]
|
||||
}
|
||||
ChoiceCategory::UppercaseLetter => {
|
||||
UPPERCASE_LETTERS[current_index]
|
||||
}
|
||||
ChoiceCategory::Digit => DIGITS[current_index],
|
||||
ChoiceCategory::SpecialSymbol => SPECIAL_SYMBOLS[current_index],
|
||||
ChoiceCategory::Menu => unreachable!(),
|
||||
};
|
||||
ButtonAction::select_item(util::char_to_string::<1>(ch))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("PassphraseEntry");
|
||||
// NOTE: `show_plain_passphrase` was not able to be transferred,
|
||||
// as it is true only for a very small amount of time
|
||||
t.kw_pair("textbox", self.textbox.content());
|
||||
self.report_btn_actions(t);
|
||||
t.field("choice_page", &self.choice_page);
|
||||
t.close();
|
||||
}
|
||||
}
|
246
core/embed/rust/src/ui/model_tr/component/pin.rs
Normal file
@ -0,0 +1,246 @@
|
||||
use crate::{
|
||||
micropython::buffer::StrBuffer,
|
||||
trezorhal::random,
|
||||
ui::{
|
||||
component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx},
|
||||
geometry::Rect,
|
||||
},
|
||||
};
|
||||
|
||||
use super::{
|
||||
choice_item::BigCharacterChoiceItem, ButtonDetails, ButtonLayout, ChangingTextLine,
|
||||
ChoiceFactory, ChoiceItem, ChoicePage, ChoicePageMsg, MultilineTextChoiceItem,
|
||||
};
|
||||
use heapless::String;
|
||||
|
||||
pub enum PinEntryMsg {
|
||||
Confirmed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
const MAX_PIN_LENGTH: usize = 50;
|
||||
const MAX_VISIBLE_DOTS: usize = 18;
|
||||
const MAX_VISIBLE_DIGITS: usize = 18;
|
||||
|
||||
const CHOICE_LENGTH: usize = 14;
|
||||
const EXIT_INDEX: usize = 0;
|
||||
const DELETE_INDEX: usize = 1;
|
||||
const SHOW_INDEX: usize = 2;
|
||||
const PROMPT_INDEX: usize = 3;
|
||||
const CHOICES: [&str; CHOICE_LENGTH] = [
|
||||
"EXIT",
|
||||
"DELETE",
|
||||
"SHOW PIN",
|
||||
"PLACEHOLDER FOR THE PROMPT",
|
||||
"0",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
];
|
||||
|
||||
struct ChoiceFactoryPIN {
|
||||
prompt: StrBuffer,
|
||||
}
|
||||
|
||||
impl ChoiceFactoryPIN {
|
||||
fn new(prompt: StrBuffer) -> Self {
|
||||
Self { prompt }
|
||||
}
|
||||
}
|
||||
|
||||
impl ChoiceFactory for ChoiceFactoryPIN {
|
||||
fn get(&self, choice_index: u8) -> ChoiceItem {
|
||||
let choice = CHOICES[choice_index as usize];
|
||||
|
||||
// Depending on whether it is a digit (one character) or a text.
|
||||
// Digits are BIG, the rest is multiline.
|
||||
let mut choice_item = if choice.len() == 1 {
|
||||
let item =
|
||||
BigCharacterChoiceItem::from_str(choice, ButtonLayout::default_three_icons());
|
||||
ChoiceItem::BigCharacter(item)
|
||||
} else {
|
||||
let item = MultilineTextChoiceItem::new(
|
||||
String::from(choice),
|
||||
ButtonLayout::default_three_icons(),
|
||||
)
|
||||
.use_delimiter(' ');
|
||||
ChoiceItem::MultilineText(item)
|
||||
};
|
||||
|
||||
// Action buttons have different middle button text
|
||||
if [EXIT_INDEX, DELETE_INDEX, SHOW_INDEX, PROMPT_INDEX].contains(&(choice_index as usize)) {
|
||||
let confirm_btn = ButtonDetails::armed_text("CONFIRM");
|
||||
choice_item.set_middle_btn(Some(confirm_btn));
|
||||
}
|
||||
|
||||
// Changing the prompt text for the wanted one
|
||||
if choice_index == PROMPT_INDEX as u8 {
|
||||
choice_item.set_text(String::from(self.prompt.as_ref()));
|
||||
}
|
||||
|
||||
choice_item
|
||||
}
|
||||
|
||||
fn count(&self) -> u8 {
|
||||
CHOICE_LENGTH as u8
|
||||
}
|
||||
}
|
||||
|
||||
/// Component for entering a PIN.
|
||||
pub struct PinEntry {
|
||||
choice_page: ChoicePage<ChoiceFactoryPIN>,
|
||||
pin_dots: Child<ChangingTextLine<String<MAX_PIN_LENGTH>>>,
|
||||
show_real_pin: bool,
|
||||
textbox: TextBox<MAX_PIN_LENGTH>,
|
||||
}
|
||||
|
||||
impl PinEntry {
|
||||
pub fn new(prompt: StrBuffer) -> Self {
|
||||
let choices = ChoiceFactoryPIN::new(prompt);
|
||||
|
||||
Self {
|
||||
choice_page: ChoicePage::new(choices)
|
||||
.with_initial_page_counter(3)
|
||||
.with_carousel(),
|
||||
pin_dots: Child::new(ChangingTextLine::center_mono(String::new())),
|
||||
show_real_pin: false,
|
||||
textbox: TextBox::empty(),
|
||||
}
|
||||
}
|
||||
|
||||
fn append_new_digit(&mut self, ctx: &mut EventCtx, page_counter: u8) {
|
||||
let digit = CHOICES[page_counter as usize];
|
||||
self.textbox.append_slice(ctx, digit);
|
||||
}
|
||||
|
||||
fn delete_last_digit(&mut self, ctx: &mut EventCtx) {
|
||||
self.textbox.delete_last(ctx);
|
||||
}
|
||||
|
||||
fn update_pin_dots(&mut self, ctx: &mut EventCtx) {
|
||||
// TODO: this is the same action as for the passphrase entry,
|
||||
// might do a common component that will handle this part,
|
||||
// (something like `SecretTextLine`)
|
||||
// also with things like shifting the dots when too many etc.
|
||||
// TODO: when the PIN is longer than fits the screen, we might show ellipsis
|
||||
if self.show_real_pin {
|
||||
let pin = String::from(self.pin());
|
||||
self.pin_dots.inner_mut().update_text(pin);
|
||||
} else {
|
||||
let mut dots: String<MAX_PIN_LENGTH> = String::new();
|
||||
for _ in 0..self.textbox.len() {
|
||||
unwrap!(dots.push_str("*"));
|
||||
}
|
||||
self.pin_dots.inner_mut().update_text(dots);
|
||||
}
|
||||
self.pin_dots.request_complete_repaint(ctx);
|
||||
}
|
||||
|
||||
pub fn pin(&self) -> &str {
|
||||
self.textbox.content()
|
||||
}
|
||||
|
||||
fn is_full(&self) -> bool {
|
||||
self.textbox.is_full()
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.textbox.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for PinEntry {
|
||||
type Msg = PinEntryMsg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
let pin_area_height = self.pin_dots.inner().needed_height();
|
||||
let (pin_area, choice_area) = bounds.split_top(pin_area_height);
|
||||
self.pin_dots.place(pin_area);
|
||||
self.choice_page.place(choice_area);
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
// Any event when showing real PIN should hide it
|
||||
if self.show_real_pin {
|
||||
self.show_real_pin = false;
|
||||
self.update_pin_dots(ctx);
|
||||
}
|
||||
|
||||
let msg = self.choice_page.event(ctx, event);
|
||||
if let Some(ChoicePageMsg::Choice(page_counter)) = msg {
|
||||
// Performing action under specific index or appending new digit
|
||||
match page_counter as usize {
|
||||
EXIT_INDEX => return Some(PinEntryMsg::Cancelled),
|
||||
DELETE_INDEX => {
|
||||
self.delete_last_digit(ctx);
|
||||
self.update_pin_dots(ctx);
|
||||
ctx.request_paint();
|
||||
}
|
||||
SHOW_INDEX => {
|
||||
self.show_real_pin = true;
|
||||
self.update_pin_dots(ctx);
|
||||
ctx.request_paint();
|
||||
}
|
||||
PROMPT_INDEX => return Some(PinEntryMsg::Confirmed),
|
||||
_ => {
|
||||
if !self.is_full() {
|
||||
self.append_new_digit(ctx, page_counter);
|
||||
self.update_pin_dots(ctx);
|
||||
// Choosing any random digit to be shown next
|
||||
let new_page_counter =
|
||||
random::uniform_between(4, (CHOICE_LENGTH - 1) as u32);
|
||||
self.choice_page
|
||||
.set_page_counter(ctx, new_page_counter as u8);
|
||||
ctx.request_paint();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.pin_dots.paint();
|
||||
self.choice_page.paint();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
use super::{ButtonAction, ButtonPos};
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for PinEntry {
|
||||
fn get_btn_action(&self, pos: ButtonPos) -> String<25> {
|
||||
match pos {
|
||||
ButtonPos::Left => ButtonAction::PrevPage.string(),
|
||||
ButtonPos::Right => ButtonAction::NextPage.string(),
|
||||
ButtonPos::Middle => {
|
||||
let current_index = self.choice_page.page_index() as usize;
|
||||
match current_index {
|
||||
EXIT_INDEX => ButtonAction::Cancel.string(),
|
||||
DELETE_INDEX => ButtonAction::Action("Delete last digit").string(),
|
||||
SHOW_INDEX => ButtonAction::Action("Show PIN").string(),
|
||||
PROMPT_INDEX => ButtonAction::Confirm.string(),
|
||||
_ => ButtonAction::select_item(CHOICES[current_index]),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("PinEntry");
|
||||
// NOTE: `show_real_pin` was not able to be transferred,
|
||||
// as it is true only for a very small amount of time
|
||||
t.kw_pair("textbox", self.textbox.content());
|
||||
self.report_btn_actions(t);
|
||||
t.field("choice_page", &self.choice_page);
|
||||
t.close();
|
||||
}
|
||||
}
|
@ -15,6 +15,8 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
use super::ButtonStyleSheet;
|
||||
|
||||
pub enum ResultPopupMsg {
|
||||
Confirmed,
|
||||
}
|
||||
@ -48,10 +50,11 @@ impl ResultPopup {
|
||||
.with_placement(LinearPlacement::vertical().align_at_center());
|
||||
|
||||
let button = button_text.map(|t| {
|
||||
// TODO: use `ButtonController` for this
|
||||
Child::new(Button::with_text(
|
||||
ButtonPos::Right,
|
||||
t,
|
||||
theme::button_default(),
|
||||
ButtonStyleSheet::default(true, false, None, None),
|
||||
))
|
||||
});
|
||||
|
||||
|
165
core/embed/rust/src/ui/model_tr/component/scrollbar.rs
Normal file
@ -0,0 +1,165 @@
|
||||
use crate::ui::{
|
||||
component::{Component, Event, EventCtx, Never, Pad},
|
||||
display,
|
||||
geometry::{Offset, Point, Rect},
|
||||
model_tr::theme,
|
||||
};
|
||||
|
||||
/// In which direction should the scrollbar be positioned
|
||||
pub enum ScrollbarOrientation {
|
||||
Vertical,
|
||||
Horizontal,
|
||||
}
|
||||
|
||||
pub struct ScrollBar {
|
||||
area: Rect,
|
||||
pad: Pad,
|
||||
pub page_count: usize,
|
||||
pub active_page: usize,
|
||||
pub orientation: ScrollbarOrientation,
|
||||
}
|
||||
|
||||
impl ScrollBar {
|
||||
pub const WIDTH: i16 = 8;
|
||||
pub const DOT_SIZE: Offset = Offset::new(4, 4);
|
||||
pub const DOT_INTERVAL: i16 = 6;
|
||||
|
||||
pub fn new(page_count: usize, orientation: ScrollbarOrientation) -> Self {
|
||||
Self {
|
||||
area: Rect::zero(),
|
||||
pad: Pad::with_background(theme::BG),
|
||||
page_count,
|
||||
active_page: 0,
|
||||
orientation,
|
||||
}
|
||||
}
|
||||
|
||||
/// Page count will be given later as it is not available yet.
|
||||
pub fn vertical_to_be_filled_later() -> Self {
|
||||
Self::vertical(0)
|
||||
}
|
||||
|
||||
pub fn vertical(page_count: usize) -> Self {
|
||||
Self::new(page_count, ScrollbarOrientation::Vertical)
|
||||
}
|
||||
|
||||
pub fn horizontal(page_count: usize) -> Self {
|
||||
Self::new(page_count, ScrollbarOrientation::Horizontal)
|
||||
}
|
||||
|
||||
pub fn set_page_count(&mut self, page_count: usize) {
|
||||
self.page_count = page_count;
|
||||
}
|
||||
|
||||
pub fn set_active_page(&mut self, active_page: usize) {
|
||||
self.active_page = active_page;
|
||||
}
|
||||
|
||||
pub fn has_next_page(&self) -> bool {
|
||||
self.active_page < self.page_count - 1
|
||||
}
|
||||
|
||||
pub fn has_previous_page(&self) -> bool {
|
||||
self.active_page > 0
|
||||
}
|
||||
|
||||
pub fn go_to_next_page(&mut self) {
|
||||
self.active_page = self.active_page.saturating_add(1).min(self.page_count - 1);
|
||||
}
|
||||
|
||||
pub fn go_to_previous_page(&mut self) {
|
||||
self.active_page = self.active_page.saturating_sub(1);
|
||||
}
|
||||
|
||||
/// Create a (seemingly circular) dot given its top left point.
|
||||
/// Make it full when it is active, otherwise paint just the perimeter and
|
||||
/// leave center empty.
|
||||
fn paint_dot(&self, active: bool, top_left: Point) {
|
||||
let full_square = Rect::from_top_left_and_size(top_left, ScrollBar::DOT_SIZE);
|
||||
|
||||
// FG - painting the full square
|
||||
display::rect_fill(full_square, theme::FG);
|
||||
|
||||
// BG - erase four corners
|
||||
for p in full_square.corner_points().iter() {
|
||||
display::paint_point(p, theme::BG);
|
||||
}
|
||||
|
||||
// BG - erasing the middle when not active
|
||||
if !active {
|
||||
display::rect_fill(full_square.shrink(1), theme::BG)
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_vertical(&mut self) {
|
||||
let count = self.page_count as i16;
|
||||
let interval = {
|
||||
let available_space = self.area.height();
|
||||
let naive_space = count * Self::DOT_INTERVAL;
|
||||
if naive_space > available_space {
|
||||
available_space / count
|
||||
} else {
|
||||
Self::DOT_INTERVAL
|
||||
}
|
||||
};
|
||||
let mut top_left = Point::new(
|
||||
self.area.center().x - Self::DOT_SIZE.x / 2,
|
||||
self.area.center().y - (count / 2) * interval,
|
||||
);
|
||||
for i in 0..self.page_count {
|
||||
self.paint_dot(i == self.active_page, top_left);
|
||||
top_left.y += interval;
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_horizontal(&mut self) {
|
||||
let count = self.page_count as i16;
|
||||
let interval = {
|
||||
let available_space = self.area.width();
|
||||
let naive_space = count * Self::DOT_INTERVAL;
|
||||
if naive_space > available_space {
|
||||
available_space / count
|
||||
} else {
|
||||
Self::DOT_INTERVAL
|
||||
}
|
||||
};
|
||||
let mut top_left = Point::new(
|
||||
self.area.center().x - (count / 2) * interval,
|
||||
self.area.center().y - Self::DOT_SIZE.y / 2,
|
||||
);
|
||||
for i in 0..self.page_count {
|
||||
self.paint_dot(i == self.active_page, top_left);
|
||||
top_left.x += interval;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for ScrollBar {
|
||||
type Msg = Never;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.area = bounds;
|
||||
self.pad.place(bounds);
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Displaying one dot for each page.
|
||||
fn paint(&mut self) {
|
||||
// Not showing the scrollbar dot when there is only one page
|
||||
if self.page_count <= 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
self.pad.clear();
|
||||
self.pad.paint();
|
||||
if matches!(self.orientation, ScrollbarOrientation::Vertical) {
|
||||
self.paint_vertical()
|
||||
} else {
|
||||
self.paint_horizontal()
|
||||
}
|
||||
}
|
||||
}
|
134
core/embed/rust/src/ui/model_tr/component/simple_choice.rs
Normal file
@ -0,0 +1,134 @@
|
||||
use crate::ui::{
|
||||
component::{Component, Event, EventCtx},
|
||||
geometry::Rect,
|
||||
};
|
||||
|
||||
use super::{ButtonLayout, ChoiceFactory, ChoiceItem, ChoicePage, ChoicePageMsg, TextChoiceItem};
|
||||
use heapless::{String, Vec};
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
use super::{ButtonAction, ButtonPos};
|
||||
|
||||
pub enum SimpleChoiceMsg {
|
||||
Result(String<50>),
|
||||
}
|
||||
|
||||
struct ChoiceFactorySimple<T, const N: usize> {
|
||||
choices: Vec<T, N>,
|
||||
}
|
||||
|
||||
impl<T, const N: usize> ChoiceFactorySimple<T, N>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
fn new(choices: Vec<T, N>) -> Self {
|
||||
Self { choices }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, const N: usize> ChoiceFactory for ChoiceFactorySimple<T, N>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
fn get(&self, choice_index: u8) -> ChoiceItem {
|
||||
let text = &self.choices[choice_index as usize];
|
||||
let text_item = TextChoiceItem::new(text, ButtonLayout::default_three_icons());
|
||||
let mut choice_item = ChoiceItem::Text(text_item);
|
||||
|
||||
// Disabling prev/next buttons for the first/last choice.
|
||||
if choice_index == 0 {
|
||||
choice_item.set_left_btn(None);
|
||||
} else if choice_index as usize == N - 1 {
|
||||
choice_item.set_right_btn(None);
|
||||
}
|
||||
|
||||
choice_item
|
||||
}
|
||||
|
||||
fn count(&self) -> u8 {
|
||||
N as u8
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple wrapper around `ChoicePage` that allows for
|
||||
/// inputting a list of values and receiving the chosen one.
|
||||
pub struct SimpleChoice<T, const N: usize>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
T: Clone,
|
||||
{
|
||||
choices: Vec<T, N>,
|
||||
choice_page: ChoicePage<ChoiceFactorySimple<T, N>>,
|
||||
}
|
||||
|
||||
impl<T, const N: usize> SimpleChoice<T, N>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
T: Clone,
|
||||
{
|
||||
pub fn new(str_choices: Vec<T, N>) -> Self {
|
||||
let choices = ChoiceFactorySimple::new(str_choices.clone());
|
||||
Self {
|
||||
choices: str_choices,
|
||||
choice_page: ChoicePage::new(choices),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, const N: usize> Component for SimpleChoice<T, N>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
T: Clone,
|
||||
{
|
||||
type Msg = SimpleChoiceMsg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.choice_page.place(bounds)
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
let msg = self.choice_page.event(ctx, event);
|
||||
match msg {
|
||||
Some(ChoicePageMsg::Choice(page_counter)) => {
|
||||
let result = String::from(self.choices[page_counter as usize].as_ref());
|
||||
Some(SimpleChoiceMsg::Result(result))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.choice_page.paint();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T, const N: usize> crate::trace::Trace for SimpleChoice<T, N>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
T: Clone,
|
||||
{
|
||||
fn get_btn_action(&self, pos: ButtonPos) -> String<25> {
|
||||
match pos {
|
||||
ButtonPos::Left => match self.choice_page.has_previous_choice() {
|
||||
true => ButtonAction::PrevPage.string(),
|
||||
false => ButtonAction::empty(),
|
||||
},
|
||||
ButtonPos::Right => match self.choice_page.has_next_choice() {
|
||||
true => ButtonAction::NextPage.string(),
|
||||
false => ButtonAction::empty(),
|
||||
},
|
||||
ButtonPos::Middle => {
|
||||
let current_index = self.choice_page.page_index() as usize;
|
||||
ButtonAction::select_item(self.choices[current_index].as_ref())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("SimpleChoice");
|
||||
self.report_btn_actions(t);
|
||||
t.field("choice_page", &self.choice_page);
|
||||
t.close();
|
||||
}
|
||||
}
|
@ -1,30 +1,47 @@
|
||||
use core::convert::TryInto;
|
||||
|
||||
use heapless::{String, Vec};
|
||||
|
||||
use crate::{
|
||||
error::Error,
|
||||
micropython::{buffer::StrBuffer, map::Map, module::Module, obj::Obj, qstr::Qstr, util},
|
||||
micropython::{
|
||||
buffer::StrBuffer,
|
||||
iter::{Iter, IterBuf},
|
||||
map::Map,
|
||||
module::Module,
|
||||
obj::Obj,
|
||||
qstr::Qstr,
|
||||
util,
|
||||
},
|
||||
time::Duration,
|
||||
ui::{
|
||||
component::{
|
||||
base::Component,
|
||||
base::{Component, ComponentExt},
|
||||
paginated::{PageMsg, Paginate},
|
||||
text::paragraphs::{Paragraph, Paragraphs},
|
||||
FormattedText,
|
||||
},
|
||||
layout::{
|
||||
obj::{ComponentMsgObj, LayoutObj},
|
||||
result::{CANCELLED, CONFIRMED, INFO},
|
||||
result::{CANCELLED, CONFIRMED, INFO}, util::iter_into_vec,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
use super::{
|
||||
component::{Button, ButtonPage, ButtonPos, Frame},
|
||||
component::{
|
||||
Bip39Entry, Bip39EntryMsg, ButtonActions, ButtonDetails, ButtonLayout, ButtonPage, Flow,
|
||||
FlowMsg, FlowPages, Frame, Page, PassphraseEntry, PassphraseEntryMsg, PinEntry,
|
||||
PinEntryMsg, SimpleChoice, SimpleChoiceMsg,
|
||||
},
|
||||
theme,
|
||||
};
|
||||
|
||||
impl<T> ComponentMsgObj for ButtonPage<T>
|
||||
impl<S, T> ComponentMsgObj for ButtonPage<S, T>
|
||||
where
|
||||
T: Component + Paginate,
|
||||
S: AsRef<str>,
|
||||
S: Clone,
|
||||
{
|
||||
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
|
||||
match msg {
|
||||
@ -35,6 +52,56 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, const M: usize> ComponentMsgObj for Flow<F, M>
|
||||
where
|
||||
F: Fn(u8) -> Page<M>,
|
||||
{
|
||||
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
|
||||
match msg {
|
||||
FlowMsg::Confirmed => Ok(CONFIRMED.as_obj()),
|
||||
FlowMsg::Cancelled => Ok(CANCELLED.as_obj()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentMsgObj for PinEntry {
|
||||
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
|
||||
match msg {
|
||||
PinEntryMsg::Confirmed => self.pin().try_into(),
|
||||
PinEntryMsg::Cancelled => Ok(CANCELLED.as_obj()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, const N: usize> ComponentMsgObj for SimpleChoice<T, N>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
T: Clone,
|
||||
{
|
||||
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
|
||||
match msg {
|
||||
SimpleChoiceMsg::Result(choice) => choice.as_str().try_into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentMsgObj for Bip39Entry {
|
||||
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
|
||||
match msg {
|
||||
Bip39EntryMsg::ResultWord(word) => word.as_str().try_into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentMsgObj for PassphraseEntry {
|
||||
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
|
||||
match msg {
|
||||
PassphraseEntryMsg::Confirmed => self.passphrase().try_into(),
|
||||
PassphraseEntryMsg::Cancelled => Ok(CANCELLED.as_obj()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> ComponentMsgObj for Frame<T, U>
|
||||
where
|
||||
T: ComponentMsgObj,
|
||||
@ -55,28 +122,53 @@ extern "C" fn new_confirm_action(n_args: usize, args: *const Obj, kwargs: *mut M
|
||||
let verb_cancel: Option<StrBuffer> =
|
||||
kwargs.get(Qstr::MP_QSTR_verb_cancel)?.try_into_option()?;
|
||||
let reverse: bool = kwargs.get(Qstr::MP_QSTR_reverse)?.try_into()?;
|
||||
let hold: bool = kwargs.get(Qstr::MP_QSTR_hold)?.try_into()?;
|
||||
|
||||
// TODO: could be replaced by Flow with one element after it supports pagination
|
||||
|
||||
let format = match (&action, &description, reverse) {
|
||||
(Some(_), Some(_), false) => "{bold}{action}\n\r{normal}{description}",
|
||||
(Some(_), Some(_), true) => "{normal}{description}\n\r{bold}{action}",
|
||||
(Some(_), None, _) => "{bold}{action}",
|
||||
(None, Some(_), _) => "{normal}{description}",
|
||||
(Some(_), Some(_), false) => "{Font::bold}{action}\n\r{Font::normal}{description}",
|
||||
(Some(_), Some(_), true) => "{Font::normal}{description}\n\r{Font::bold}{action}",
|
||||
(Some(_), None, _) => "{Font::bold}{action}",
|
||||
(None, Some(_), _) => "{Font::normal}{description}",
|
||||
_ => "",
|
||||
};
|
||||
|
||||
let _left = verb_cancel
|
||||
.map(|label| Button::with_text(ButtonPos::Left, label, theme::button_cancel()));
|
||||
let _right =
|
||||
verb.map(|label| Button::with_text(ButtonPos::Right, label, theme::button_default()));
|
||||
let verb_cancel = verb_cancel.unwrap_or_default();
|
||||
let verb = verb.unwrap_or_default();
|
||||
|
||||
let cancel_btn = if verb_cancel.len() > 0 {
|
||||
Some(ButtonDetails::cancel_icon())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut confirm_btn = if verb.len() > 0 {
|
||||
Some(ButtonDetails::text(verb))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Optional HoldToConfirm
|
||||
if hold {
|
||||
// TODO: clients might want to set the duration
|
||||
confirm_btn = confirm_btn.map(|btn| btn.with_duration(Duration::from_secs(2)));
|
||||
}
|
||||
|
||||
// TODO: make sure the text will not be colliding with the buttons
|
||||
// - make there some space on the bottom of the text
|
||||
|
||||
let obj = LayoutObj::new(Frame::new(
|
||||
title,
|
||||
ButtonPage::new(
|
||||
None,
|
||||
ButtonPage::new_str_buf(
|
||||
FormattedText::new(theme::TEXT_NORMAL, theme::FORMATTED, format)
|
||||
.with("action", action.unwrap_or_default())
|
||||
.with("description", description.unwrap_or_default()),
|
||||
theme::BG,
|
||||
),
|
||||
)
|
||||
.with_cancel_btn(cancel_btn)
|
||||
.with_confirm_btn(confirm_btn),
|
||||
))?;
|
||||
Ok(obj.into())
|
||||
};
|
||||
@ -90,9 +182,12 @@ extern "C" fn new_confirm_text(n_args: usize, args: *const Obj, kwargs: *mut Map
|
||||
let description: Option<StrBuffer> =
|
||||
kwargs.get(Qstr::MP_QSTR_description)?.try_into_option()?;
|
||||
|
||||
// TODO: could be replaced by Flow with one element after it supports pagination
|
||||
|
||||
let obj = LayoutObj::new(Frame::new(
|
||||
title,
|
||||
ButtonPage::new(
|
||||
None,
|
||||
ButtonPage::new_str(
|
||||
Paragraphs::new([
|
||||
Paragraph::new(&theme::TEXT_NORMAL, description.unwrap_or_default()),
|
||||
Paragraph::new(&theme::TEXT_BOLD, data),
|
||||
@ -105,6 +200,401 @@ extern "C" fn new_confirm_text(n_args: usize, args: *const Obj, kwargs: *mut Map
|
||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||
}
|
||||
|
||||
extern "C" fn confirm_output(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||
let block = |_args: &[Obj], kwargs: &Map| {
|
||||
let address: StrBuffer = kwargs.get(Qstr::MP_QSTR_address)?.try_into()?;
|
||||
// Getting this from micropython so it is also a `StrBuffer`, not having
|
||||
// to handle the string operation in Rust, which would make it a `String`
|
||||
// (which would them cause issues with general `T: AsRef<str>` parameter)
|
||||
let truncated_address: StrBuffer =
|
||||
kwargs.get(Qstr::MP_QSTR_truncated_address)?.try_into()?;
|
||||
let amount: StrBuffer = kwargs.get(Qstr::MP_QSTR_amount)?.try_into()?;
|
||||
let title: StrBuffer = "Send".into();
|
||||
|
||||
let get_page = move |page_index| {
|
||||
// Showing two screens - the recipient address and summary confirmation
|
||||
match page_index {
|
||||
0 => {
|
||||
// `icon + label + address`
|
||||
let btn_layout = ButtonLayout::new(
|
||||
Some(ButtonDetails::cancel_icon()),
|
||||
None,
|
||||
Some(ButtonDetails::text("CONTINUE")),
|
||||
);
|
||||
let btn_actions = ButtonActions::cancel_next();
|
||||
Page::<20>::new(btn_layout, btn_actions).icon_label_text(
|
||||
theme::ICON_USER,
|
||||
"Recipient".into(),
|
||||
address.clone(),
|
||||
)
|
||||
}
|
||||
1 => {
|
||||
// 2 pairs `icon + label + text`
|
||||
let btn_layout = ButtonLayout::new(
|
||||
Some(ButtonDetails::cancel_icon()),
|
||||
None,
|
||||
Some(
|
||||
ButtonDetails::text("HOLD TO CONFIRM")
|
||||
.with_duration(Duration::from_secs(2)),
|
||||
),
|
||||
);
|
||||
let btn_actions = ButtonActions::cancel_confirm();
|
||||
Page::<20>::new(btn_layout, btn_actions)
|
||||
.icon_label_text(
|
||||
theme::ICON_USER,
|
||||
"Recipient".into(),
|
||||
truncated_address.clone(),
|
||||
)
|
||||
.newline()
|
||||
.icon_label_text(theme::ICON_AMOUNT, "Amount".into(), amount.clone())
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
};
|
||||
let pages = FlowPages::new(get_page, 2);
|
||||
|
||||
let obj = LayoutObj::new(Flow::new(pages).with_common_title(title).into_child())?;
|
||||
Ok(obj.into())
|
||||
};
|
||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||
}
|
||||
|
||||
extern "C" fn confirm_total(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||
let block = |_args: &[Obj], kwargs: &Map| {
|
||||
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
|
||||
let total_amount: StrBuffer = kwargs.get(Qstr::MP_QSTR_total_amount)?.try_into()?;
|
||||
let fee_amount: StrBuffer = kwargs.get(Qstr::MP_QSTR_fee_amount)?.try_into()?;
|
||||
let fee_rate_amount: Option<StrBuffer> = kwargs
|
||||
.get(Qstr::MP_QSTR_fee_rate_amount)?
|
||||
.try_into_option()?;
|
||||
let total_label: StrBuffer = kwargs.get(Qstr::MP_QSTR_total_label)?.try_into()?;
|
||||
let fee_label: StrBuffer = kwargs.get(Qstr::MP_QSTR_fee_label)?.try_into()?;
|
||||
|
||||
let get_page = move |page_index| {
|
||||
// One page with 2 or 3 pairs `icon + label + text`
|
||||
assert!(page_index == 0);
|
||||
|
||||
let btn_layout = ButtonLayout::new(
|
||||
Some(ButtonDetails::cancel_icon()),
|
||||
None,
|
||||
Some(ButtonDetails::text("HOLD TO SEND").with_duration(Duration::from_secs(2))),
|
||||
);
|
||||
let btn_actions = ButtonActions::cancel_confirm();
|
||||
|
||||
let mut flow_page = Page::<25>::new(btn_layout, btn_actions)
|
||||
.icon_label_text(theme::ICON_PARAM, total_label.clone(), total_amount.clone())
|
||||
.newline()
|
||||
.icon_label_text(theme::ICON_PARAM, fee_label.clone(), fee_amount.clone());
|
||||
|
||||
if let Some(fee_rate_amount) = &fee_rate_amount {
|
||||
flow_page = flow_page.newline().icon_label_text(
|
||||
theme::ICON_PARAM,
|
||||
"Fee rate".into(),
|
||||
fee_rate_amount.clone(),
|
||||
)
|
||||
}
|
||||
flow_page
|
||||
};
|
||||
let pages = FlowPages::new(get_page, 1);
|
||||
|
||||
let obj = LayoutObj::new(Flow::new(pages).with_common_title(title).into_child())?;
|
||||
Ok(obj.into())
|
||||
};
|
||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||
}
|
||||
|
||||
extern "C" fn tutorial(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||
let block = |_args: &[Obj], _kwargs: &Map| {
|
||||
let get_page = |page_index| {
|
||||
// Lazy-loaded list of screens to show, with custom content,
|
||||
// buttons and actions triggered by these buttons.
|
||||
// Cancelling the first screen will point to the last one,
|
||||
// which asks for confirmation whether user wants to
|
||||
// really cancel the tutorial.
|
||||
let screen = match page_index {
|
||||
// title, text, btn_layout, btn_actions
|
||||
0 => (
|
||||
"Hello!",
|
||||
"Welcome to Trezor.\n\n\nPress right to continue.",
|
||||
ButtonLayout::cancel_and_arrow(),
|
||||
ButtonActions::last_next(),
|
||||
),
|
||||
1 => (
|
||||
"Basics",
|
||||
"Use Trezor by clicking left & right.\nPress right to continue.",
|
||||
ButtonLayout::left_right_arrows(),
|
||||
ButtonActions::prev_next(),
|
||||
),
|
||||
2 => (
|
||||
"Confirm",
|
||||
"Press both left & right at the same time to confirm.",
|
||||
ButtonLayout::new(
|
||||
Some(ButtonDetails::left_arrow_icon()),
|
||||
Some(ButtonDetails::armed_text("CONFIRM")),
|
||||
None,
|
||||
),
|
||||
ButtonActions::prev_next_with_middle(),
|
||||
),
|
||||
3 => (
|
||||
"Hold to confirm",
|
||||
"Press & hold right to approve important operations.",
|
||||
ButtonLayout::new(
|
||||
Some(ButtonDetails::left_arrow_icon()),
|
||||
None,
|
||||
Some(
|
||||
ButtonDetails::text("HOLD TO CONFIRM")
|
||||
.with_duration(Duration::from_millis(2000)),
|
||||
),
|
||||
),
|
||||
ButtonActions::prev_next(),
|
||||
),
|
||||
// TODO: merge these two scrolls into one, with using a scrollbar
|
||||
4 => (
|
||||
"Screen scroll",
|
||||
"Press right to scroll down to read all content when text doesn't...",
|
||||
ButtonLayout::new(
|
||||
Some(ButtonDetails::left_arrow_icon()),
|
||||
None,
|
||||
Some(ButtonDetails::down_arrow_icon_wide()),
|
||||
),
|
||||
ButtonActions::prev_next(),
|
||||
),
|
||||
5 => (
|
||||
"Screen scroll",
|
||||
"fit on one screen. Press left to scroll up.",
|
||||
ButtonLayout::new(
|
||||
Some(ButtonDetails::up_arrow_icon_wide()),
|
||||
None,
|
||||
Some(ButtonDetails::text("CONFIRM")),
|
||||
),
|
||||
ButtonActions::prev_next(),
|
||||
),
|
||||
6 => (
|
||||
"Congrats!",
|
||||
"You're ready to use Trezor.",
|
||||
ButtonLayout::new(
|
||||
Some(ButtonDetails::text("AGAIN")),
|
||||
None,
|
||||
Some(ButtonDetails::text("FINISH")),
|
||||
),
|
||||
ButtonActions::beginning_confirm(),
|
||||
),
|
||||
7 => (
|
||||
"Skip tutorial?",
|
||||
"Sure you want to skip the tutorial?",
|
||||
ButtonLayout::new(
|
||||
Some(ButtonDetails::left_arrow_icon()),
|
||||
None,
|
||||
Some(ButtonDetails::text("CONFIRM")),
|
||||
),
|
||||
ButtonActions::beginning_cancel(),
|
||||
),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
Page::<10>::new(screen.2.clone(), screen.3.clone())
|
||||
.text_bold(screen.0.into())
|
||||
.newline()
|
||||
.newline_half()
|
||||
.text_normal(screen.1.into())
|
||||
};
|
||||
|
||||
let pages = FlowPages::new(get_page, 8);
|
||||
|
||||
let obj = LayoutObj::new(Flow::new(pages).into_child())?;
|
||||
Ok(obj.into())
|
||||
};
|
||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||
}
|
||||
|
||||
extern "C" fn pin_confirm_action(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||
let block = |_args: &[Obj], kwargs: &Map| {
|
||||
let action: StrBuffer = kwargs.get(Qstr::MP_QSTR_action)?.try_into()?;
|
||||
|
||||
let get_page = move |page_index| {
|
||||
let screen = match page_index {
|
||||
// title, text, btn_layout, btn_actions
|
||||
// NOTE: doing the newlines manually to look exactly same
|
||||
// as in the design.
|
||||
0 => (
|
||||
"PIN settings".into(),
|
||||
"PIN should\ncontain at\nleast four\ndigits",
|
||||
ButtonLayout::cancel_and_text("GOT IT"),
|
||||
ButtonActions::cancel_next(),
|
||||
),
|
||||
1 => (
|
||||
action.clone(),
|
||||
"You'll use\nthis PIN to\naccess this\ndevice.",
|
||||
ButtonLayout::cancel_and_htc_text(
|
||||
"HOLD TO CONFIRM",
|
||||
Duration::from_millis(1000),
|
||||
),
|
||||
ButtonActions::cancel_confirm(),
|
||||
),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
Page::<10>::new(screen.2.clone(), screen.3.clone())
|
||||
.text_bold(screen.0)
|
||||
.newline()
|
||||
.newline_half()
|
||||
.text_normal(screen.1.into())
|
||||
};
|
||||
let pages = FlowPages::new(get_page, 2);
|
||||
|
||||
let obj = LayoutObj::new(Flow::new(pages).into_child())?;
|
||||
Ok(obj.into())
|
||||
};
|
||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||
}
|
||||
|
||||
extern "C" fn request_pin(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||
let block = |_args: &[Obj], kwargs: &Map| {
|
||||
let prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?;
|
||||
let _subprompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_subprompt)?.try_into()?;
|
||||
let _allow_cancel: Option<bool> =
|
||||
kwargs.get(Qstr::MP_QSTR_allow_cancel)?.try_into_option()?;
|
||||
|
||||
let obj = LayoutObj::new(PinEntry::new(prompt).into_child())?;
|
||||
Ok(obj.into())
|
||||
};
|
||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||
}
|
||||
|
||||
extern "C" fn show_share_words(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||
let block = |_args: &[Obj], kwargs: &Map| {
|
||||
let share_words_obj: Obj = kwargs.get(Qstr::MP_QSTR_share_words)?;
|
||||
let title = "Recovery seed";
|
||||
|
||||
// Parsing the list of share words.
|
||||
// Assume there is always up to 24 words in the newly generated seed
|
||||
// (for now, later we might support SLIP39 with up to 33 words)
|
||||
let mut iter_buf = IterBuf::new();
|
||||
let iter_words = Iter::try_from_obj_with_buf(share_words_obj, &mut iter_buf)?;
|
||||
let mut share_words: Vec<StrBuffer, 24> = Vec::new();
|
||||
for word in iter_words {
|
||||
share_words.push(word.try_into()?).unwrap();
|
||||
}
|
||||
|
||||
let share_words_len = share_words.len() as u8;
|
||||
|
||||
let beginning_text = build_string!(
|
||||
40,
|
||||
"Write down these ",
|
||||
inttostr!(share_words_len),
|
||||
" words:\n\n"
|
||||
);
|
||||
|
||||
let mut middle_words: String<360> = String::new();
|
||||
// Vec<StrBuffer> does not support `enumerate()`
|
||||
let mut index: u8 = 0;
|
||||
for word in share_words {
|
||||
index += 1;
|
||||
let line = build_string!(15, inttostr!(index), ". ", word.as_ref(), "\n");
|
||||
|
||||
middle_words.push_str(&line).unwrap();
|
||||
}
|
||||
|
||||
let end_text = build_string!(
|
||||
40,
|
||||
"I wrote down all ",
|
||||
inttostr!(share_words_len),
|
||||
" words in order."
|
||||
);
|
||||
|
||||
// TODO: instead of this could create a new paragraph for the beginning,
|
||||
// each word and the end
|
||||
let text_to_show = build_string!(
|
||||
440,
|
||||
beginning_text.as_str(),
|
||||
middle_words.as_str(),
|
||||
end_text.as_str()
|
||||
);
|
||||
|
||||
// Adding hold-to-confirm button at the end
|
||||
// Also no possibility of cancelling
|
||||
let cancel_btn = None;
|
||||
let confirm_btn =
|
||||
Some(ButtonDetails::text("CONFIRM").with_duration(Duration::from_secs(2)));
|
||||
|
||||
let obj = LayoutObj::new(Frame::new(
|
||||
title,
|
||||
None,
|
||||
ButtonPage::new_str(
|
||||
Paragraphs::new(
|
||||
[
|
||||
Paragraph::new(&theme::TEXT_BOLD, text_to_show)
|
||||
]
|
||||
),
|
||||
theme::BG,
|
||||
)
|
||||
.with_cancel_btn(cancel_btn)
|
||||
.with_confirm_btn(confirm_btn),
|
||||
))?;
|
||||
Ok(obj.into())
|
||||
};
|
||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||
}
|
||||
|
||||
extern "C" fn select_word(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||
let block = |_args: &[Obj], kwargs: &Map| {
|
||||
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
|
||||
let description: StrBuffer = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?;
|
||||
let words_iterable: Obj = kwargs.get(Qstr::MP_QSTR_words)?;
|
||||
let words: Vec<StrBuffer, 3> = iter_into_vec(words_iterable)?;
|
||||
|
||||
let obj = LayoutObj::new(Frame::new(
|
||||
title,
|
||||
Some(description),
|
||||
SimpleChoice::new(words).into_child(),
|
||||
))?;
|
||||
Ok(obj.into())
|
||||
};
|
||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||
}
|
||||
|
||||
extern "C" fn request_word_count(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||
let block = |_args: &[Obj], kwargs: &Map| {
|
||||
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
|
||||
let text: StrBuffer = kwargs.get(Qstr::MP_QSTR_text)?.try_into()?;
|
||||
|
||||
let choices: Vec<&str, 5> = ["12", "18", "20", "24", "33"].into_iter().collect();
|
||||
|
||||
let obj = LayoutObj::new(Frame::new(
|
||||
title,
|
||||
Some(text),
|
||||
SimpleChoice::new(choices).into_child(),
|
||||
))?;
|
||||
Ok(obj.into())
|
||||
};
|
||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||
}
|
||||
|
||||
extern "C" fn request_word_bip39(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||
let block = |_args: &[Obj], kwargs: &Map| {
|
||||
let prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?;
|
||||
|
||||
let obj = LayoutObj::new(Frame::new(prompt, None, Bip39Entry::new().into_child()))?;
|
||||
Ok(obj.into())
|
||||
};
|
||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||
}
|
||||
|
||||
extern "C" fn request_passphrase(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||
let block = |_args: &[Obj], kwargs: &Map| {
|
||||
let prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?;
|
||||
let _max_len: u8 = kwargs.get(Qstr::MP_QSTR_max_len)?.try_into()?;
|
||||
|
||||
let obj = LayoutObj::new(Frame::new(
|
||||
prompt,
|
||||
None,
|
||||
PassphraseEntry::new().into_child(),
|
||||
))?;
|
||||
Ok(obj.into())
|
||||
};
|
||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub static mp_module_trezorui2: Module = obj_module! {
|
||||
Qstr::MP_QSTR___name__ => Qstr::MP_QSTR_trezorui2.to_obj(),
|
||||
@ -125,12 +615,50 @@ pub static mp_module_trezorui2: Module = obj_module! {
|
||||
/// description: str | None = None,
|
||||
/// verb: str | None = None,
|
||||
/// verb_cancel: str | None = None,
|
||||
/// hold: bool | None = None,
|
||||
/// hold: bool = False,
|
||||
/// reverse: bool = False,
|
||||
/// ) -> object:
|
||||
/// """Confirm action."""
|
||||
Qstr::MP_QSTR_confirm_action => obj_fn_kw!(0, new_confirm_action).as_obj(),
|
||||
|
||||
/// def confirm_output_r(
|
||||
/// *,
|
||||
/// address: str,
|
||||
/// truncated_address: str,
|
||||
/// amount: str,
|
||||
/// ) -> object:
|
||||
/// """Confirm output. Specific for model R."""
|
||||
Qstr::MP_QSTR_confirm_output_r => obj_fn_kw!(0, confirm_output).as_obj(),
|
||||
|
||||
/// def confirm_total_r(
|
||||
/// *,
|
||||
/// title: str,
|
||||
/// total_amount: str,
|
||||
/// fee_amount: str,
|
||||
/// fee_rate_amount: str | None = None,
|
||||
/// total_label: str,
|
||||
/// fee_label: str,
|
||||
/// ) -> object:
|
||||
/// """Confirm summary of a transaction. Specific for model R."""
|
||||
Qstr::MP_QSTR_confirm_total_r => obj_fn_kw!(0, confirm_total).as_obj(),
|
||||
|
||||
/// def tutorial() -> object:
|
||||
/// """Show user how to interact with the device."""
|
||||
Qstr::MP_QSTR_tutorial => obj_fn_kw!(0, tutorial).as_obj(),
|
||||
|
||||
/// def pin_confirm_action(*, action: str) -> object:
|
||||
/// """Confirm PIN action and informing user about it."""
|
||||
Qstr::MP_QSTR_pin_confirm_action => obj_fn_kw!(0, pin_confirm_action).as_obj(),
|
||||
|
||||
/// def request_pin(
|
||||
/// *,
|
||||
/// prompt: str,
|
||||
/// subprompt: str | None = None,
|
||||
/// allow_cancel: bool | None = None,
|
||||
/// ) -> str | object:
|
||||
/// """Request pin on device."""
|
||||
Qstr::MP_QSTR_request_pin => obj_fn_kw!(0, request_pin).as_obj(),
|
||||
|
||||
/// def confirm_text(
|
||||
/// *,
|
||||
/// title: str,
|
||||
@ -139,103 +667,143 @@ pub static mp_module_trezorui2: Module = obj_module! {
|
||||
/// ) -> object:
|
||||
/// """Confirm text."""
|
||||
Qstr::MP_QSTR_confirm_text => obj_fn_kw!(0, new_confirm_text).as_obj(),
|
||||
|
||||
/// def show_share_words(
|
||||
/// *,
|
||||
/// share_words: Iterable[str],
|
||||
/// ) -> None:
|
||||
/// """Shows a backup seed."""
|
||||
Qstr::MP_QSTR_show_share_words => obj_fn_kw!(0, show_share_words).as_obj(),
|
||||
|
||||
/// def select_word(
|
||||
/// *,
|
||||
/// title: str,
|
||||
/// description: str,
|
||||
/// words: Iterable[str],
|
||||
/// ) -> int:
|
||||
/// """Select mnemonic word from three possibilities - seed check after backup. The
|
||||
/// iterable must be of exact size. Returns index in range `0..3`."""
|
||||
Qstr::MP_QSTR_select_word => obj_fn_kw!(0, select_word).as_obj(),
|
||||
|
||||
/// def request_word_count(
|
||||
/// *,
|
||||
/// title: str,
|
||||
/// text: str,
|
||||
/// ) -> str: # TODO: make it return int
|
||||
/// """Get word count for recovery."""
|
||||
Qstr::MP_QSTR_request_word_count => obj_fn_kw!(0, request_word_count).as_obj(),
|
||||
|
||||
/// def request_word_bip39(
|
||||
/// *,
|
||||
/// prompt: str,
|
||||
/// ) -> str:
|
||||
/// """Get recovery word for BIP39."""
|
||||
Qstr::MP_QSTR_request_word_bip39 => obj_fn_kw!(0, request_word_bip39).as_obj(),
|
||||
|
||||
/// def request_passphrase(
|
||||
/// *,
|
||||
/// prompt: str,
|
||||
/// max_len: int,
|
||||
/// ) -> str:
|
||||
/// """Get passphrase."""
|
||||
Qstr::MP_QSTR_request_passphrase => obj_fn_kw!(0, request_passphrase).as_obj(),
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
trace::Trace,
|
||||
ui::{
|
||||
component::Component,
|
||||
model_tr::{
|
||||
component::{Dialog, DialogMsg},
|
||||
constant,
|
||||
},
|
||||
},
|
||||
};
|
||||
// use crate::{
|
||||
// trace::Trace,
|
||||
// ui::{
|
||||
// component::Component,
|
||||
// model_tr::{
|
||||
// component::{Dialog, DialogMsg},
|
||||
// constant,
|
||||
// },
|
||||
// },
|
||||
// };
|
||||
|
||||
use super::*;
|
||||
// use super::*;
|
||||
|
||||
fn trace(val: &impl Trace) -> String {
|
||||
let mut t = Vec::new();
|
||||
val.trace(&mut t);
|
||||
String::from_utf8(t).unwrap()
|
||||
}
|
||||
// fn trace(val: &impl Trace) -> String {
|
||||
// let mut t = Vec::new();
|
||||
// val.trace(&mut t);
|
||||
// String::from_utf8(t).unwrap()
|
||||
// }
|
||||
|
||||
impl<T, U> ComponentMsgObj for Dialog<T, U>
|
||||
where
|
||||
T: ComponentMsgObj,
|
||||
U: AsRef<str>,
|
||||
{
|
||||
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
|
||||
match msg {
|
||||
DialogMsg::Content(c) => self.inner().msg_try_into_obj(c),
|
||||
DialogMsg::LeftClicked => Ok(CANCELLED.as_obj()),
|
||||
DialogMsg::RightClicked => Ok(CONFIRMED.as_obj()),
|
||||
}
|
||||
}
|
||||
}
|
||||
// impl<T, U> ComponentMsgObj for Dialog<T, U>
|
||||
// where
|
||||
// T: ComponentMsgObj,
|
||||
// U: AsRef<str>,
|
||||
// {
|
||||
// fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error>
|
||||
// { match msg {
|
||||
// DialogMsg::Content(c) =>
|
||||
// self.inner().msg_try_into_obj(c),
|
||||
// DialogMsg::LeftClicked => Ok(CANCELLED.as_obj()),
|
||||
// DialogMsg::RightClicked => Ok(CONFIRMED.as_obj()), }
|
||||
// }
|
||||
// }
|
||||
|
||||
#[test]
|
||||
fn trace_example_layout() {
|
||||
let mut layout = Dialog::new(
|
||||
FormattedText::new(
|
||||
theme::TEXT_NORMAL,
|
||||
theme::FORMATTED,
|
||||
"Testing text layout, with some text, and some more text. And {param}",
|
||||
)
|
||||
.with("param", "parameters!"),
|
||||
Some(Button::with_text(
|
||||
ButtonPos::Left,
|
||||
"Left",
|
||||
theme::button_cancel(),
|
||||
)),
|
||||
Some(Button::with_text(
|
||||
ButtonPos::Right,
|
||||
"Right",
|
||||
theme::button_default(),
|
||||
)),
|
||||
);
|
||||
layout.place(constant::screen());
|
||||
assert_eq!(
|
||||
trace(&layout),
|
||||
r#"<Dialog content:<Text content:Testing text layout,
|
||||
with some text, and
|
||||
some more text. And p-
|
||||
arameters! > left:<Button text:Left > right:<Button text:Right > >"#
|
||||
)
|
||||
}
|
||||
// #[test]
|
||||
// fn trace_example_layout() {
|
||||
// let mut layout = Dialog::new(
|
||||
// FormattedText::new(
|
||||
// theme::TEXT_NORMAL,
|
||||
// theme::FORMATTED,
|
||||
// "Testing text layout, with some text, and some more text.
|
||||
// And {param}", )
|
||||
// .with("param", "parameters!"),
|
||||
// Some(Button::with_text(
|
||||
// ButtonPos::Left,
|
||||
// "Left",
|
||||
// theme::button_cancel(),
|
||||
// )),
|
||||
// Some(Button::with_text(
|
||||
// ButtonPos::Right,
|
||||
// "Right",
|
||||
// theme::button_default(),
|
||||
// )),
|
||||
// );
|
||||
// layout.place(constant::screen());
|
||||
// assert_eq!(
|
||||
// trace(&layout),
|
||||
// r#"<Dialog content:<Text content:Testing text layout,
|
||||
// with some text, and
|
||||
// some more text. And p-
|
||||
// arameters! > left:<Button text:Left > right:<Button text:Right > >"#
|
||||
// )
|
||||
// }
|
||||
|
||||
#[test]
|
||||
fn trace_layout_title() {
|
||||
let mut layout = Frame::new(
|
||||
"Please confirm",
|
||||
Dialog::new(
|
||||
FormattedText::new(
|
||||
theme::TEXT_NORMAL,
|
||||
theme::FORMATTED,
|
||||
"Testing text layout, with some text, and some more text. And {param}",
|
||||
)
|
||||
.with("param", "parameters!"),
|
||||
Some(Button::with_text(
|
||||
ButtonPos::Left,
|
||||
"Left",
|
||||
theme::button_cancel(),
|
||||
)),
|
||||
Some(Button::with_text(
|
||||
ButtonPos::Right,
|
||||
"Right",
|
||||
theme::button_default(),
|
||||
)),
|
||||
),
|
||||
);
|
||||
layout.place(constant::screen());
|
||||
assert_eq!(
|
||||
trace(&layout),
|
||||
r#"<Frame title:Please confirm content:<Dialog content:<Text content:Testing text layout,
|
||||
with some text, and
|
||||
some more text. And p-
|
||||
arameters! > left:<Button text:Left > right:<Button text:Right > > >"#
|
||||
)
|
||||
}
|
||||
// #[test]
|
||||
// fn trace_layout_title() {
|
||||
// let mut layout = Frame::new(
|
||||
// "Please confirm",
|
||||
// Dialog::new(
|
||||
// FormattedText::new(
|
||||
// theme::TEXT_NORMAL,
|
||||
// theme::FORMATTED,
|
||||
// "Testing text layout, with some text, and some more
|
||||
// text. And {param}", )
|
||||
// .with("param", "parameters!"),
|
||||
// Some(Button::with_text(
|
||||
// ButtonPos::Left,
|
||||
// "Left",
|
||||
// theme::button_cancel(),
|
||||
// )),
|
||||
// Some(Button::with_text(
|
||||
// ButtonPos::Right,
|
||||
// "Right",
|
||||
// theme::button_default(),
|
||||
// )),
|
||||
// ),
|
||||
// );
|
||||
// layout.place(constant::screen());
|
||||
// assert_eq!(
|
||||
// trace(&layout),
|
||||
// r#"<Frame title:Please confirm content:<Dialog content:<Text
|
||||
// content:Testing text layout, with some text, and
|
||||
// some more text. And p-
|
||||
// arameters! > left:<Button text:Left > right:<Button text:Right > > >"#
|
||||
// )
|
||||
// }
|
||||
}
|
||||
|