hackathon_input_flows
grdddj 1 year ago
parent 393bc48b6b
commit eb4075f934

@ -133,6 +133,37 @@ core fw btconly production build:
- firmware-T2T1-btconly-production-*.*.*-$CI_COMMIT_SHORT_SHA.bin
expire_in: 1 week
core fw R debug build:
stage: build
<<: *gitlab_caching
needs: []
variables:
TREZOR_MODEL: "R"
PYOPT: "0"
script:
- nix-shell --run "poetry run make -C core build_firmware"
- cp core/build/firmware/firmware.bin trezor-fw-debug-tr-$CORE_VERSION-$CI_COMMIT_SHORT_SHA.bin
artifacts:
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
paths:
- trezor-fw-*.*.*-$CI_COMMIT_SHORT_SHA.bin
expire_in: 1 week
core fw R build:
stage: build
<<: *gitlab_caching
needs: []
variables:
TREZOR_MODEL: "R"
script:
- nix-shell --run "poetry run make -C core build_firmware"
- cp core/build/firmware/firmware.bin trezor-fw-tr-$CORE_VERSION-$CI_COMMIT_SHORT_SHA.bin
artifacts:
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
paths:
- trezor-fw-*.*.*-$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:
@ -235,6 +266,38 @@ core unix frozen debug build:
untracked: true
expire_in: 1 week
core unix frozen R debug build:
stage: build
<<: *gitlab_caching
needs: []
variables:
PYOPT: "0"
TREZOR_MODEL: "R"
script:
- nix-shell --run "poetry run make -C core build_unix_frozen"
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
core unix frozen debug asan build:
stage: build
<<: *gitlab_caching

@ -238,6 +238,7 @@ ui tests fixtures deploy:
- core device test
- core persistence test
- legacy device test
- core device R test
script:
- echo "Deploying to $DEPLOY_PATH"
- rsync --delete -va ci/ui_test_records/* "$DEPLOY_PATH"

@ -81,6 +81,37 @@ core device test:
reports:
junit: tests/junit.xml
core device R test:
stage: test
<<: *gitlab_caching
needs:
- core unix frozen R debug build
variables:
TREZOR_PROFILING: "1"
TREZOR_MODEL: "R"
script:
- nix-shell --run "poetry run make -C core test_emu_ui | ts -s"
after_script:
- mv tests/ui_tests/reporting/reports/test/ test_ui_report
- nix-shell --run "poetry run python ci/prepare_ui_artifacts.py TR | ts -s"
- diff tests/ui_tests/fixtures.json tests/ui_tests/fixtures.suggestion.json
- nix-shell --run "cd tests/ui_tests ; poetry run python reporting/report_master_diff.py TR_"
- mv tests/ui_tests/reporting/reports/master_diff/ .
artifacts:
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
paths:
- ci/ui_test_records/
- test_ui_report
- tests/ui_tests/screens/
- tests/ui_tests/fixtures.suggestion.json
- tests/junit.xml
- tests/trezor.log
- master_diff
when: always
expire_in: 4 weeks
reports:
junit: tests/junit.xml
core device asan test:
stage: test
<<: *gitlab_caching
@ -293,6 +324,36 @@ core click test:
expire_in: 1 week
when: always
# Click tests.
# See [docs/tests/click-tests](../tests/click-tests.md) for more info.
core click R test:
stage: test
<<: *gitlab_caching
needs:
- core unix frozen R debug build
variables:
TREZOR_PROFILING: 1
script:
- nix-shell --run "poetry run make -C core test_emu_click_ui | ts -s"
after_script:
- mv core/src/.coverage core/.coverage.test_click
- mv tests/ui_tests/reports/test/ test_ui_report
- nix-shell --run "poetry run python ci/prepare_ui_artifacts.py | ts -s"
- diff -u tests/ui_tests/fixtures.json tests/ui_tests/fixtures.suggestion.json
artifacts:
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
paths:
- ci/ui_test_records/
- test_ui_report
- tests/ui_tests/screens/
- tests/ui_tests/fixtures.suggestion.json
- tests/trezor.log
- tests/junit.xml
reports:
junit: tests/junit.xml
expire_in: 1 week
when: always
core click asan test:
stage: test
<<: *gitlab_caching

@ -17,32 +17,43 @@ import "messages-management.proto";
* @next DebugLinkLayout
*/
message DebugLinkDecision {
optional DebugButton button = 1; // button press
optional DebugSwipeDirection swipe = 2; // swipe direction
optional string input = 3; // keyboard input
/**
* Structure representing swipe direction
*/
enum DebugSwipeDirection {
UP = 0;
DOWN = 1;
LEFT = 2;
RIGHT = 3;
}
/**
* Structure representing button presses
*/
enum DebugButton {
NO = 0;
YES = 1;
INFO = 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 DebugButton button = 1; // button press
optional DebugSwipeDirection swipe = 2; // swipe direction
optional string input = 3; // keyboard input
/**
* Structure representing swipe direction
*/
enum DebugSwipeDirection {
UP = 0;
DOWN = 1;
LEFT = 2;
RIGHT = 3;
}
/**
* Structure representing button presses
*/
enum DebugButton {
NO = 0;
YES = 1;
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
}
/**
@ -50,7 +61,7 @@ message DebugLinkDecision {
* @end
*/
message DebugLinkLayout {
repeated string tokens = 1;
repeated string tokens = 1;
}
/**
@ -59,7 +70,7 @@ message DebugLinkLayout {
* @next Success
*/
message DebugLinkReseedRandom {
optional uint32 value = 1;
optional uint32 value = 1;
}
/**
@ -68,8 +79,10 @@ message DebugLinkReseedRandom {
* @next Success
*/
message DebugLinkRecordScreen {
optional string target_directory = 1; // empty or missing to stop recording
optional uint32 refresh_index = 2 [default=0]; // which index to give the screenshots (after emulator restarts)
optional string target_directory = 1; // empty or missing to stop recording
optional uint32 refresh_index = 2
[default =
0]; // which index to give the screenshots (after emulator restarts)
}
/**
@ -78,9 +91,11 @@ message DebugLinkRecordScreen {
* @next DebugLinkState
*/
message DebugLinkGetState {
optional bool wait_word_list = 1; // Trezor T only - wait until mnemonic words are shown
optional bool wait_word_pos = 2; // Trezor T only - wait until reset word position is requested
optional bool wait_layout = 3; // wait until current layout changes
optional bool wait_word_list =
1; // Trezor T only - wait until mnemonic words are shown
optional bool wait_word_pos =
2; // Trezor T only - wait until reset word position is requested
optional bool wait_layout = 3; // wait until current layout changes
}
/**
@ -88,36 +103,44 @@ message DebugLinkGetState {
* @end
*/
message DebugLinkState {
optional bytes layout = 1; // raw buffer of display
optional string pin = 2; // current PIN, blank if PIN is not set/enabled
optional string matrix = 3; // current PIN matrix
optional bytes mnemonic_secret = 4; // current mnemonic secret
optional common.HDNodeType node = 5; // current BIP-32 node
optional bool passphrase_protection = 6; // is node/mnemonic encrypted using passphrase?
optional string reset_word = 7; // word on device display during ResetDevice workflow
optional bytes reset_entropy = 8; // current entropy during ResetDevice workflow
optional string recovery_fake_word = 9; // (fake) word on display during RecoveryDevice workflow
optional uint32 recovery_word_pos = 10; // index of mnemonic word the device is expecting during RecoveryDevice workflow
optional uint32 reset_word_pos = 11; // index of mnemonic word the device is expecting during ResetDevice workflow
optional management.BackupType mnemonic_type = 12; // current mnemonic type (BIP-39/SLIP-39)
repeated string tokens = 13; // current layout represented as a list of string tokens
optional bytes layout = 1; // raw buffer of display
optional string pin = 2; // current PIN, blank if PIN is not set/enabled
optional string matrix = 3; // current PIN matrix
optional bytes mnemonic_secret = 4; // current mnemonic secret
optional common.HDNodeType node = 5; // current BIP-32 node
optional bool passphrase_protection =
6; // is node/mnemonic encrypted using passphrase?
optional string reset_word =
7; // word on device display during ResetDevice workflow
optional bytes reset_entropy =
8; // current entropy during ResetDevice workflow
optional string recovery_fake_word =
9; // (fake) word on display during RecoveryDevice workflow
optional uint32 recovery_word_pos =
10; // index of mnemonic word the device is expecting during
// RecoveryDevice workflow
optional uint32 reset_word_pos = 11; // index of mnemonic word the device is
// expecting during ResetDevice workflow
optional management.BackupType mnemonic_type =
12; // current mnemonic type (BIP-39/SLIP-39)
repeated string tokens =
13; // current layout represented as a list of string tokens
}
/**
* Request: Ask device to restart
* @start
*/
message DebugLinkStop {
}
message DebugLinkStop {}
/**
* Response: Device wants host to log event
* @ignore
*/
message DebugLinkLog {
optional uint32 level = 1;
optional string bucket = 2;
optional string text = 3;
optional uint32 level = 1;
optional string bucket = 2;
optional string text = 3;
}
/**
@ -126,8 +149,8 @@ message DebugLinkLog {
* @next DebugLinkMemory
*/
message DebugLinkMemoryRead {
optional uint32 address = 1;
optional uint32 length = 2;
optional uint32 address = 1;
optional uint32 length = 2;
}
/**
@ -135,7 +158,7 @@ message DebugLinkMemoryRead {
* @end
*/
message DebugLinkMemory {
optional bytes memory = 1;
optional bytes memory = 1;
}
/**
@ -146,9 +169,9 @@ message DebugLinkMemory {
* @next Failure
*/
message DebugLinkMemoryWrite {
optional uint32 address = 1;
optional bytes memory = 2;
optional bool flash = 3;
optional uint32 address = 1;
optional bytes memory = 2;
optional bool flash = 3;
}
/**
@ -159,10 +182,9 @@ message DebugLinkMemoryWrite {
* @next Failure
*/
message DebugLinkFlashErase {
optional uint32 sector = 1;
optional uint32 sector = 1;
}
/**
* Request: Erase the SD card
* @start
@ -170,26 +192,23 @@ message DebugLinkFlashErase {
* @next Failure
*/
message DebugLinkEraseSdCard {
optional bool format = 1; // if true, the card will be formatted to FAT32.
// if false, it will be all 0xFF bytes.
optional bool format = 1; // if true, the card will be formatted to FAT32.
// if false, it will be all 0xFF bytes.
}
/**
* Request: Start or stop tracking layout changes
* @start
* @next Success
*/
message DebugLinkWatchLayout {
optional bool watch = 1; // if true, start watching layout.
// if false, stop.
optional bool watch = 1; // if true, start watching layout.
// if false, stop.
}
/**
* Request: Remove all the previous debug event state
* @start
* @next Success
*/
message DebugLinkResetDebugEvents {
}
message DebugLinkResetDebugEvents {}

@ -502,3 +502,11 @@ message UnlockPath {
message UnlockedPathRequest {
optional bytes mac = 1; // authentication code for future UnlockPath calls
}
/**
* Request: Show tutorial screens on the device
* @start
* @next Success
*/
message ShowDeviceTutorial {
}

@ -120,6 +120,7 @@ enum MessageType {
reserved 90 to 92;
MessageType_UnlockPath = 93 [(bitcoin_only) = true, (wire_in) = true];
MessageType_UnlockedPathRequest = 94 [(bitcoin_only) = true, (wire_out) = true];
MessageType_ShowDeviceTutorial = 95 [(bitcoin_only) = true, (wire_in) = true];
MessageType_SetU2FCounter = 63 [(wire_in) = true];
MessageType_GetNextU2FCounter = 80 [(wire_in) = true];

@ -29,9 +29,9 @@ SOURCE_MOD = []
PYOPT = ARGUMENTS.get('PYOPT', '1')
FROZEN = True
if TREZOR_MODEL in ('1', 'R'):
FONT_NORMAL='Font_PixelOperator_Regular_8'
FONT_DEMIBOLD=None
if TREZOR_MODEL in ('R',):
FONT_NORMAL='Font_Unifont_Regular_16'
FONT_DEMIBOLD='Font_Unifont_Bold_16'
FONT_BOLD='Font_PixelOperator_Bold_8'
FONT_MONO='Font_PixelOperatorMono_Regular_8'
if TREZOR_MODEL in ('T', ):
@ -571,11 +571,13 @@ if FROZEN:
SOURCE_PY_DIR = 'src/'
SOURCE_PY = Glob(SOURCE_PY_DIR + '*.py')
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/*.py',
exclude=[
SOURCE_PY_DIR + 'trezor/sdcard.py',
] if TREZOR_MODEL not in ('T',) else []
))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/crypto/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/common/*.py'))
# UI layouts - common files and then model-specific. Exclude FIDO when BTC-only.
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/*.py',
@ -589,14 +591,22 @@ if FROZEN:
SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/fido.py',
] if not EVERYTHING else []
))
elif TREZOR_MODEL in ('1', 'R'):
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tr/__init__.py'))
elif TREZOR_MODEL in ('R'):
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tr/*.py',
exclude=[
SOURCE_PY_DIR + 'trezor/ui/layouts/tr/fido.py',
] if not EVERYTHING else []
))
else:
raise ValueError('Unknown Trezor model')
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/wire/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'storage/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'storage/*.py',
exclude=[
SOURCE_PY_DIR + 'storage/sd_salt.py',
] if TREZOR_MODEL not in ('T',) else []
))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/messages/__init__.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/*.py',
@ -616,8 +626,11 @@ if FROZEN:
)
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/*.py'))
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/common/*.py',
exclude=[
SOURCE_PY_DIR + 'apps/common/sdcard.py',
] if TREZOR_MODEL not in ('T',) else []
))
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/management/*.py',

@ -25,9 +25,9 @@ PYOPT = ARGUMENTS.get('PYOPT', '1')
FROZEN = ARGUMENTS.get('TREZOR_EMULATOR_FROZEN', 0)
RASPI = os.getenv('TREZOR_EMULATOR_RASPI') == '1'
if TREZOR_MODEL in ('1', 'R'):
FONT_NORMAL='Font_PixelOperator_Regular_8'
FONT_DEMIBOLD='Font_PixelOperator_Regular_8'
if TREZOR_MODEL in ('R',):
FONT_NORMAL='Font_Unifont_Regular_16'
FONT_DEMIBOLD='Font_Unifont_Bold_16'
FONT_BOLD='Font_PixelOperator_Bold_8'
FONT_MONO='Font_PixelOperatorMono_Regular_8'
if TREZOR_MODEL in ('T', ):
@ -353,7 +353,7 @@ SOURCE_UNIX = [
'vendor/micropython/ports/unix/input.c',
'vendor/micropython/ports/unix/unix_mphal.c',
]
if TREZOR_MODEL in ('T',):
if TREZOR_MODEL in ('T', 'R'):
SOURCE_UNIX += [
'embed/unix/sbu.c',
'embed/unix/sdcard.c',
@ -542,11 +542,13 @@ if FROZEN:
SOURCE_PY_DIR = 'src/'
SOURCE_PY = Glob(SOURCE_PY_DIR + '*.py')
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/*.py',
exclude=[
SOURCE_PY_DIR + 'trezor/sdcard.py',
] if TREZOR_MODEL not in ('T',) else []
))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/crypto/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/common/*.py'))
# UI layouts - common files and then model-specific. Exclude FIDO when BTC-only.
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/*.py',
@ -560,14 +562,22 @@ if FROZEN:
SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/fido.py',
] if not EVERYTHING else []
))
elif TREZOR_MODEL in ('1', 'R'):
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tr/__init__.py'))
elif TREZOR_MODEL in ('R'):
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tr/*.py',
exclude=[
SOURCE_PY_DIR + 'trezor/ui/layouts/tr/fido.py',
] if not EVERYTHING else []
))
else:
raise ValueError('Unknown Trezor model')
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/wire/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'storage/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'storage/*.py',
exclude=[
SOURCE_PY_DIR + 'storage/sd_salt.py',
] if TREZOR_MODEL not in ('T',) else []
))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/messages/__init__.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/*.py',
@ -587,8 +597,11 @@ if FROZEN:
)
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/*.py'))
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/common/*.py',
exclude=[
SOURCE_PY_DIR + 'apps/common/sdcard.py',
] if TREZOR_MODEL not in ('T',) else []
))
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/management/*.py',

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 B

@ -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 }; // width hand-changed from 8 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 };

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

@ -0,0 +1,10 @@
#include <stdint.h>
#if TREZOR_FONT_BPP != 1
#error Wrong TREZOR_FONT_BPP (expected 1)
#endif
#define Font_Unifont_Bold_16_HEIGHT 12 // <--- 12 from 16
#define Font_Unifont_Bold_16_MAX_HEIGHT 12 // <--- 12 from 15
#define Font_Unifont_Bold_16_BASELINE 2
extern const uint8_t* const Font_Unifont_Bold_16[126 + 1 - 32];
extern const uint8_t Font_Unifont_Bold_16_glyph_nonprintable[];

@ -0,0 +1,207 @@
#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
// MANUAL CHANGES!
// In cases where the width and advance were the same (usually 7 and 7), increasing
// the advance to 8, so that these wide letters do not collide with the following one.
/* */ 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, 8, 0, 10, 16, 250, 76, 135, 3, 132, 201, 124, 32 }; // < --- advanced changed from 7 to 8
/* % */ static const uint8_t Font_Unifont_Regular_16_glyph_37[] = { 7, 10, 8, 0, 10, 99, 42, 83, 65, 2, 11, 41, 83, 24 }; // < --- advanced changed from 7 to 8
/* & */ static const uint8_t Font_Unifont_Regular_16_glyph_38[] = { 7, 10, 8, 0, 10, 56, 137, 17, 67, 10, 98, 194, 140, 228 }; // < --- advanced changed from 7 to 8
/* ' */ 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, 8, 0, 8, 17, 37, 81, 197, 82, 68, 0 }; // < --- advanced changed from 7 to 8
/* + */ static const uint8_t Font_Unifont_Regular_16_glyph_43[] = { 7, 7, 8, 0, 8, 16, 32, 71, 241, 2, 4, 0 }; // < --- advanced changed from 7 to 8
/* , */ 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, 8, 0, 10, 62, 16, 32, 64, 129, 2, 68, 136, 224 }; // < --- advanced changed from 7 to 8
/* 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, 8, 0, 10, 121, 10, 20, 40, 80, 161, 90, 204, 240, 24 }; // < --- advanced changed from 7 to 8
/* 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, 8, 0, 10, 254, 32, 64, 129, 2, 4, 8, 16, 32 }; // < --- advanced changed from 7 to 8
/* 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, 8, 0, 10, 131, 6, 10, 36, 72, 138, 20, 16, 32 }; // < --- advanced changed from 7 to 8
/* 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, 8, 0, 10, 131, 5, 18, 34, 130, 4, 8, 16, 32 }; // < --- advanced changed from 7 to 8
/* 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, 8, 0, 0, 254 }; // < --- advanced changed from 7 to 8
/* ` */ 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, 8, 0, 8, 237, 38, 76, 153, 50, 100, 201, 0 }; // < --- advanced changed from 7 to 8
/* 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, 8, 0, 8, 131, 38, 76, 153, 50, 100, 182, 0 }; // < --- advanced changed from 7 to 8
/* 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, 8, 0, 11, 99, 38, 48 }; // < --- advanced changed from 7 to 8
const uint8_t Font_Unifont_Regular_16_glyph_nonprintable[] = { 6, 10, 7, 0, 10, 133, 231, 190, 247, 190, 255, 239, 191 };
const uint8_t * const Font_Unifont_Regular_16[126 + 1 - 32] = {
Font_Unifont_Regular_16_glyph_32,
Font_Unifont_Regular_16_glyph_33,
Font_Unifont_Regular_16_glyph_34,
Font_Unifont_Regular_16_glyph_35,
Font_Unifont_Regular_16_glyph_36,
Font_Unifont_Regular_16_glyph_37,
Font_Unifont_Regular_16_glyph_38,
Font_Unifont_Regular_16_glyph_39,
Font_Unifont_Regular_16_glyph_40,
Font_Unifont_Regular_16_glyph_41,
Font_Unifont_Regular_16_glyph_42,
Font_Unifont_Regular_16_glyph_43,
Font_Unifont_Regular_16_glyph_44,
Font_Unifont_Regular_16_glyph_45,
Font_Unifont_Regular_16_glyph_46,
Font_Unifont_Regular_16_glyph_47,
Font_Unifont_Regular_16_glyph_48,
Font_Unifont_Regular_16_glyph_49,
Font_Unifont_Regular_16_glyph_50,
Font_Unifont_Regular_16_glyph_51,
Font_Unifont_Regular_16_glyph_52,
Font_Unifont_Regular_16_glyph_53,
Font_Unifont_Regular_16_glyph_54,
Font_Unifont_Regular_16_glyph_55,
Font_Unifont_Regular_16_glyph_56,
Font_Unifont_Regular_16_glyph_57,
Font_Unifont_Regular_16_glyph_58,
Font_Unifont_Regular_16_glyph_59,
Font_Unifont_Regular_16_glyph_60,
Font_Unifont_Regular_16_glyph_61,
Font_Unifont_Regular_16_glyph_62,
Font_Unifont_Regular_16_glyph_63,
Font_Unifont_Regular_16_glyph_64,
Font_Unifont_Regular_16_glyph_65,
Font_Unifont_Regular_16_glyph_66,
Font_Unifont_Regular_16_glyph_67,
Font_Unifont_Regular_16_glyph_68,
Font_Unifont_Regular_16_glyph_69,
Font_Unifont_Regular_16_glyph_70,
Font_Unifont_Regular_16_glyph_71,
Font_Unifont_Regular_16_glyph_72,
Font_Unifont_Regular_16_glyph_73,
Font_Unifont_Regular_16_glyph_74,
Font_Unifont_Regular_16_glyph_75,
Font_Unifont_Regular_16_glyph_76,
Font_Unifont_Regular_16_glyph_77,
Font_Unifont_Regular_16_glyph_78,
Font_Unifont_Regular_16_glyph_79,
Font_Unifont_Regular_16_glyph_80,
Font_Unifont_Regular_16_glyph_81,
Font_Unifont_Regular_16_glyph_82,
Font_Unifont_Regular_16_glyph_83,
Font_Unifont_Regular_16_glyph_84,
Font_Unifont_Regular_16_glyph_85,
Font_Unifont_Regular_16_glyph_86,
Font_Unifont_Regular_16_glyph_87,
Font_Unifont_Regular_16_glyph_88,
Font_Unifont_Regular_16_glyph_89,
Font_Unifont_Regular_16_glyph_90,
Font_Unifont_Regular_16_glyph_91,
Font_Unifont_Regular_16_glyph_92,
Font_Unifont_Regular_16_glyph_93,
Font_Unifont_Regular_16_glyph_94,
Font_Unifont_Regular_16_glyph_95,
Font_Unifont_Regular_16_glyph_96,
Font_Unifont_Regular_16_glyph_97,
Font_Unifont_Regular_16_glyph_98,
Font_Unifont_Regular_16_glyph_99,
Font_Unifont_Regular_16_glyph_100,
Font_Unifont_Regular_16_glyph_101,
Font_Unifont_Regular_16_glyph_102,
Font_Unifont_Regular_16_glyph_103,
Font_Unifont_Regular_16_glyph_104,
Font_Unifont_Regular_16_glyph_105,
Font_Unifont_Regular_16_glyph_106,
Font_Unifont_Regular_16_glyph_107,
Font_Unifont_Regular_16_glyph_108,
Font_Unifont_Regular_16_glyph_109,
Font_Unifont_Regular_16_glyph_110,
Font_Unifont_Regular_16_glyph_111,
Font_Unifont_Regular_16_glyph_112,
Font_Unifont_Regular_16_glyph_113,
Font_Unifont_Regular_16_glyph_114,
Font_Unifont_Regular_16_glyph_115,
Font_Unifont_Regular_16_glyph_116,
Font_Unifont_Regular_16_glyph_117,
Font_Unifont_Regular_16_glyph_118,
Font_Unifont_Regular_16_glyph_119,
Font_Unifont_Regular_16_glyph_120,
Font_Unifont_Regular_16_glyph_121,
Font_Unifont_Regular_16_glyph_122,
Font_Unifont_Regular_16_glyph_123,
Font_Unifont_Regular_16_glyph_124,
Font_Unifont_Regular_16_glyph_125,
Font_Unifont_Regular_16_glyph_126,
};

@ -0,0 +1,10 @@
#include <stdint.h>
#if TREZOR_FONT_BPP != 1
#error Wrong TREZOR_FONT_BPP (expected 1)
#endif
#define Font_Unifont_Regular_16_HEIGHT 12 // <--- 12 from 16
#define Font_Unifont_Regular_16_MAX_HEIGHT 12 // <--- 12 from 15
#define Font_Unifont_Regular_16_BASELINE 2
extern const uint8_t* const Font_Unifont_Regular_16[126 + 1 - 32];
extern const uint8_t Font_Unifont_Regular_16_glyph_nonprintable[];

@ -9,7 +9,7 @@ build = "build.rs"
default = ["model_tt"]
bitcoin_only = []
model_tt = ["jpeg"]
model_tr = []
model_tr = ["toif"]
micropython = []
protobuf = ["micropython"]
ui = []
@ -21,13 +21,23 @@ button = []
touch = []
clippy = []
jpeg = []
disp_i8080_8bit_dw = [] # write pixels directly to peripheral
disp_i8080_16bit_dw = [] # write pixels directly to peripheral
toif = []
disp_i8080_8bit_dw = [] # write pixels directly to peripheral
disp_i8080_16bit_dw = [] # write pixels directly to peripheral
debug = ["ui_debug"]
sbu = []
sd_card = []
rgb_led = []
test = ["cc", "glob", "micropython", "protobuf", "ui", "ui_debug", "dma2d", "touch"]
test = [
"cc",
"glob",
"micropython",
"protobuf",
"ui",
"ui_debug",
"dma2d",
"touch",
]
[lib]
crate-type = ["staticlib"]

@ -20,9 +20,12 @@ static void _librust_qstrs(void) {
MP_QSTR_action;
MP_QSTR_active;
MP_QSTR_address;
MP_QSTR_address_title;
MP_QSTR_allow_cancel;
MP_QSTR_amount;
MP_QSTR_amount_change;
MP_QSTR_amount_new;
MP_QSTR_amount_title;
MP_QSTR_app_name;
MP_QSTR_attach_timer_fn;
MP_QSTR_bootscreen;
@ -36,13 +39,14 @@ static void _librust_qstrs(void) {
MP_QSTR_confirm_coinjoin;
MP_QSTR_confirm_fido;
MP_QSTR_confirm_homescreen;
MP_QSTR_confirm_joint_total;
MP_QSTR_confirm_modify_fee;
MP_QSTR_confirm_modify_output;
MP_QSTR_confirm_more;
MP_QSTR_confirm_output;
MP_QSTR_confirm_properties;
MP_QSTR_confirm_recovery;
MP_QSTR_confirm_reset_device;
MP_QSTR_confirm_text;
MP_QSTR_confirm_total;
MP_QSTR_confirm_value;
MP_QSTR_confirm_with_info;
@ -53,6 +57,8 @@ static void _librust_qstrs(void) {
MP_QSTR_draw_welcome_screen;
MP_QSTR_dry_run;
MP_QSTR_extra;
MP_QSTR_fee_amount;
MP_QSTR_fee_label;
MP_QSTR_fee_rate;
MP_QSTR_fee_rate_amount;
MP_QSTR_hold;
@ -78,6 +84,7 @@ static void _librust_qstrs(void) {
MP_QSTR_pages;
MP_QSTR_paint;
MP_QSTR_path;
MP_QSTR_place;
MP_QSTR_progress_event;
MP_QSTR_prompt;
MP_QSTR_request_bip39;
@ -89,6 +96,7 @@ static void _librust_qstrs(void) {
MP_QSTR_reverse;
MP_QSTR_select_word;
MP_QSTR_select_word_count;
MP_QSTR_share_words;
MP_QSTR_show_address_details;
MP_QSTR_show_checklist;
MP_QSTR_show_error;
@ -107,15 +115,20 @@ static void _librust_qstrs(void) {
MP_QSTR_show_warning;
MP_QSTR_sign;
MP_QSTR_skip_first_paint;
MP_QSTR_spending_amount;
MP_QSTR_subprompt;
MP_QSTR_subtitle;
MP_QSTR_time_ms;
MP_QSTR_timer;
MP_QSTR_title;
MP_QSTR_toif_info;
MP_QSTR_total_amount;
MP_QSTR_total_fee_new;
MP_QSTR_total_label;
MP_QSTR_touch_event;
MP_QSTR_trace;
MP_QSTR_trezorui2;
MP_QSTR_tutorial;
MP_QSTR_usb_event;
MP_QSTR_user_fee_change;
MP_QSTR_value;

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

@ -1,5 +1,11 @@
use crate::strutil::format_i64;
// TODO: try to move these things into TR's code
#[cfg(feature = "model_tr")]
use crate::ui::model_tr::component::ButtonPos;
#[cfg(feature = "model_tr")]
use heapless::String;
pub trait Tracer {
fn child(&mut self, key: &str, value: &dyn Trace);
fn int(&mut self, key: &str, i: i64);
@ -192,6 +198,18 @@ impl<F: FnMut(&str)> Tracer for JsonTracer<F> {
/// interface.
pub trait Trace {
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.string("left_action", &self.get_btn_action(ButtonPos::Left));
t.string("middle_action", &self.get_btn_action(ButtonPos::Middle));
t.string("right_action", &self.get_btn_action(ButtonPos::Right));
}
}
#[cfg(test)]

@ -9,3 +9,41 @@ pub fn shuffle<T>(slice: &mut [T]) {
slice.swap(i, j);
}
}
/// Returns a random number in the range [min, max].
pub fn uniform_between(min: u32, max: u32) -> u32 {
assert!(max > min);
uniform(max - min + 1) + min
}
/// Returns a random number in the range [min, max] except one `except` number.
pub fn uniform_between_except(min: u32, max: u32, except: u32) -> u32 {
// Generate uniform_between as long as it is not except
loop {
let rand = uniform_between(min, max);
if rand != except {
return rand;
}
}
}
#[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)));
}
}
#[test]
fn uniform_between_except_test() {
for _ in 0..10 {
assert!(uniform_between_except(10, 12, 11) != 11);
}
}
}

@ -17,6 +17,25 @@ impl Wordlist {
Self(unsafe { &ffi::SLIP39_WORDLIST })
}
/// Returns all possible letters that form a valid word together with some
/// prefix. Alphabetically sorted.
pub fn get_available_letters(&self, prefix: &str) -> impl Iterator<Item = char> {
// TODO: consider returning -> Vec<char, 26>?
// Fill a "set" of all unique characters, not sorted yet
let mut suffixes: heapless::Vec<char, 26> = heapless::Vec::new();
for word in self.iter() {
if word.starts_with(prefix) && word.len() > prefix.len() {
let following_char = unwrap!(word.chars().nth(prefix.len()));
if !suffixes.contains(&following_char) {
unwrap!(suffixes.push(following_char));
}
}
}
suffixes.sort_unstable();
suffixes.into_iter()
}
/// Only leaves words that have a specified prefix. Throw away others.
pub fn filter_prefix(&self, prefix: &str) -> Self {
let mut start = 0usize;
@ -158,4 +177,23 @@ mod tests {
.collect::<Vec<_>>();
assert_eq!(result, expected_result);
}
#[test]
fn test_get_available_letters() {
let expected_result = vec!['a', 'i', 'l', 'o', 's', 'u'];
let result = Wordlist::bip39()
.get_available_letters("ab")
.collect::<Vec<_>>();
assert_eq!(result, expected_result);
let expected_result = vec!['a', 'e', 'i', 'o', 'u'];
let result = Wordlist::bip39()
.get_available_letters("str")
.collect::<Vec<_>>();
assert_eq!(result, expected_result);
let result = Wordlist::bip39()
.get_available_letters("zoo")
.collect::<Vec<_>>();
assert_eq!(result.len(), 0);
}
}

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

@ -54,6 +54,10 @@ where
}
}
pub fn set_text(&mut self, text: T) {
self.text = text;
}
pub fn start(&mut self, ctx: &mut EventCtx, now: Instant) {
// Not starting if animations are disabled.
if animation_disabled() {

@ -1,8 +1,3 @@
use crate::ui::component::{
text::layout::{LayoutFit, TextNoOp},
FormattedText,
};
pub enum AuxPageMsg {
/// Page component was instantiated with BACK button on every page and it
/// was pressed.
@ -34,61 +29,3 @@ pub trait Paginate {
/// Navigate to the given page.
fn change_page(&mut self, active_page: usize);
}
impl<F, T> Paginate for FormattedText<F, T>
where
F: AsRef<str>,
T: AsRef<str>,
{
fn page_count(&mut self) -> usize {
let mut page_count = 1; // There's always at least one page.
let mut char_offset = 0;
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);
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);
}
}
}
}
}

@ -343,7 +343,7 @@ impl TextLayout {
}
/// Overall height of the content, including paddings.
fn layout_height(&self, init_cursor: Point, end_cursor: Point) -> i16 {
pub 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)
@ -557,7 +557,7 @@ struct Span {
}
impl Span {
fn fit_horizontally(
pub fn fit_horizontally(
text: &str,
max_width: i16,
text_font: impl GlyphMetrics,

@ -629,6 +629,17 @@ where
}
}
impl<T> Paginate for Checklist<T>
where
T: ParagraphSource,
{
fn page_count(&mut self) -> usize {
1
}
fn change_page(&mut self, _to_page: usize) {}
}
#[cfg(feature = "ui_debug")]
impl<T: ParagraphSource> crate::trace::Trace for Checklist<T> {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {

@ -129,6 +129,45 @@ impl Font {
display::text_width(text, self.into())
}
/// Width of the text that is visible.
/// Not including the spaces before the first and after the last character.
pub fn visible_text_width(self, text: &str) -> i16 {
match text.len() {
0 => 0,
1 => {
let char = unwrap!(text.chars().last());
let char_glyph = unwrap!(self.get_glyph(char as u8));
char_glyph.width
}
_ => {
let first_char = unwrap!(text.chars().next());
let first_char_glyph = unwrap!(self.get_glyph(first_char as u8));
let first_char_visible_width = first_char_glyph.adv - first_char_glyph.bearing_x;
let middle_chars = &text[1..text.len() - 1];
let middle_chars_width = self.text_width(middle_chars);
let last_char = unwrap!(text.chars().last());
let last_char_glyph = unwrap!(self.get_glyph(last_char as u8));
let last_char_visible_width = last_char_glyph.width + last_char_glyph.bearing_x;
first_char_visible_width + middle_chars_width + last_char_visible_width
}
}
}
/// Returning the x-bearing (offset) of the first character.
/// Useful to enforce that the text is positioned correctly (e.g. centered).
pub fn start_x_bearing(self, text: &str) -> i16 {
if text.is_empty() {
return 0;
}
let first_char = unwrap!(text.chars().next());
let first_char_glyph = unwrap!(self.get_glyph(first_char as u8));
first_char_glyph.bearing_x
}
pub fn char_width(self, ch: char) -> i16 {
display::char_width(ch, self.into())
}

@ -5,9 +5,11 @@ pub mod loader;
pub mod tjpgd;
pub mod toif;
use heapless::Vec;
use super::{
constant,
geometry::{Offset, Point, Rect},
geometry::{Alignment, Offset, Point, Rect},
};
#[cfg(feature = "dma2d")]
use crate::trezorhal::{
@ -26,7 +28,13 @@ use crate::ui::geometry::TOP_LEFT;
use crate::{
time::Duration,
trezorhal::{buffers, display, time, uzlib::UzlibContext},
ui::lerp::Lerp,
ui::{
component::text::{
layout::{Op, TextLayout, TextRenderer},
TextStyle,
},
lerp::Lerp,
},
};
// Reexports
@ -126,16 +134,16 @@ pub fn rect_fill_corners(r: Rect, fg_color: Color) {
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub struct TextOverlay<'a> {
pub struct TextOverlay<T> {
area: Rect,
text: &'a str,
text: T,
font: Font,
max_height: i16,
baseline: i16,
}
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 {
@ -147,8 +155,17 @@ impl<'a> TextOverlay<'a> {
}
}
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);
@ -167,7 +184,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 top = self.max_height - self.baseline - g.bearing_y;
let char_area = Rect::new(
Point::new(tot_adv + g.bearing_x, top),
@ -755,9 +777,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,
@ -830,6 +852,37 @@ pub fn dotted_line(start: Point, width: i16, color: Color) {
}
}
/// Draws longer multiline texts inside an area.
/// Splits lines on word boundaries/whitespace.
/// When a word is too long to fit one line, splitting
/// it on multiple lines with "-" at the line-ends.
///
/// If it fits, returns the rest of the area.
/// If it does not fit, returns `None`.
pub fn text_multiline_split_words(
area: Rect,
text: &str,
font: Font,
fg_color: Color,
bg_color: Color,
alignment: Alignment,
) -> Option<Rect> {
let text_style = TextStyle::new(font, fg_color, bg_color, fg_color, fg_color);
let text_layout = TextLayout::new(text_style)
.with_bounds(area)
.with_align(alignment);
let mut cursor = area.top_left() + Offset::y(font.line_height());
let mut ops: Vec<Op, 1> = Vec::new();
unwrap!(ops.push(Op::Text(text)));
text_layout.layout_ops(&mut ops.into_iter(), &mut cursor, &mut TextRenderer);
let drawn_height = cursor.y - area.top_left().y;
if drawn_height >= area.height() {
None
} else {
Some(area.split_top(drawn_height).1)
}
}
/// Display text left-aligned to a certain Point
pub fn text_left(baseline: Point, text: &str, font: Font, fg_color: Color, bg_color: Color) {
display::text(
@ -855,7 +908,7 @@ pub fn text_center(baseline: Point, text: &str, font: Font, fg_color: Color, bg_
);
}
/// Display text right-alligned to a certain Point
/// Display text right-aligned to a certain Point
pub fn text_right(baseline: Point, text: &str, font: Font, fg_color: Color, bg_color: Color) {
let w = font.text_width(text);
display::text(
@ -869,7 +922,6 @@ pub fn text_right(baseline: Point, text: &str, font: Font, fg_color: Color, bg_c
}
pub fn text_top_left(position: Point, text: &str, font: Font, fg_color: Color, bg_color: Color) {
// let w = font.text_width(text);
let h = font.text_height();
display::text(
position.x,
@ -881,6 +933,29 @@ pub fn text_top_left(position: Point, text: &str, font: Font, fg_color: Color, b
);
}
/// Fill background before drawing text.
/// Useful when drawing text on something already existing (like homescreen).
pub fn fill_background_for_text(
baseline: Point,
text: &str,
font: Font,
color: Color,
alignment: Alignment,
pixel_margin: i16,
) {
let width = font.text_width(text);
let baseline_x = match alignment {
Alignment::Start => baseline.x,
Alignment::Center => baseline.x - width / 2,
Alignment::End => baseline.x - width,
};
let left_bottom_point = Point::new(baseline_x, baseline.y);
let rect =
Rect::from_bottom_left_and_size(left_bottom_point, Offset::new(width, font.text_height()))
.expand(pixel_margin);
rect_fill(rect, color);
}
#[inline(always)]
pub fn pixeldata(color: Color) {
display::pixeldata(color.into());

@ -26,7 +26,11 @@ use super::Color;
const TOIF_HEADER_LENGTH: usize = 12;
pub fn icon(icon: &Icon, center: Point, fg_color: Color, bg_color: Color) {
let r = Rect::from_center_and_size(center, icon.toif.size());
icon_from_toif(&icon.toif, center, fg_color, bg_color);
}
pub fn icon_from_toif(toif: &Toif, center: Point, fg_color: Color, bg_color: Color) {
let r = Rect::from_center_and_size(center, toif.size());
let area = r.translate(get_offset());
let clamped = area.clamp(constant::screen());
let colortable = get_color_table(fg_color, bg_color);
@ -36,7 +40,7 @@ pub fn icon(icon: &Icon, center: Point, fg_color: Color, bg_color: Color) {
let mut dest = [0_u8; 1];
let mut window = [0; UZLIB_WINDOW_SIZE];
let mut ctx = icon.toif.decompression_context(Some(&mut window));
let mut ctx = toif.decompression_context(Some(&mut window));
for py in area.y0..area.y1 {
for px in area.x0..area.x1 {

@ -9,8 +9,14 @@ pub enum PhysicalButton {
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum ButtonEvent {
/// Button pressed down.
/// ▼ * | * ▼
ButtonPressed(PhysicalButton),
/// Button released up.
/// ▲ * | * ▲
ButtonReleased(PhysicalButton),
HoldStarted,
HoldEnded,
}
impl ButtonEvent {

@ -241,6 +241,21 @@ impl Rect {
}
}
pub const fn from_top_right_and_size(p0: Point, size: Offset) -> Self {
let top_left = Point::new(p0.x - size.x, p0.y);
Self::from_top_left_and_size(top_left, size)
}
pub const fn from_bottom_left_and_size(p0: Point, size: Offset) -> Self {
let top_left = Point::new(p0.x, p0.y - size.y);
Self::from_top_left_and_size(top_left, size)
}
pub const fn from_bottom_right_and_size(p0: Point, size: Offset) -> Self {
let top_left = Point::new(p0.x - size.x, p0.y - size.y);
Self::from_top_left_and_size(top_left, size)
}
pub const fn from_center_and_size(p: Point, size: Offset) -> Self {
let x0 = p.x - size.x / 2;
let y0 = p.y - size.y / 2;
@ -306,6 +321,14 @@ 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
@ -341,6 +364,11 @@ impl Rect {
}
}
/// Move all the sides further from the center by the same distance.
pub const fn expand(&self, size: i16) -> Self {
self.outset(Insets::uniform(size))
}
/// 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))
@ -386,6 +414,16 @@ impl Rect {
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),

@ -191,6 +191,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.
@ -248,6 +256,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(),
@ -432,6 +441,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()?;

@ -19,14 +19,15 @@ use crate::{
use cstr_core::cstr;
use heapless::Vec;
#[cfg(feature = "jpeg")]
use crate::{
micropython::{
buffer::get_buffer,
ffi::{mp_obj_new_int, mp_obj_new_tuple},
},
ui::display::tjpgd::{jpeg_info, jpeg_test},
#[cfg(any(feature = "toif", feature = "jpeg"))]
use crate::micropython::{
buffer::get_buffer,
ffi::{mp_obj_new_int, mp_obj_new_tuple},
};
#[cfg(feature = "jpeg")]
use crate::ui::display::tjpgd::{jpeg_info, jpeg_test};
#[cfg(feature = "toif")]
use crate::ui::display::toif::Toif;
pub fn iter_into_objs<const N: usize>(iterable: Obj) -> Result<[Obj; N], Error> {
let err = Error::ValueError(cstr!("Invalid iterable length"));
@ -40,6 +41,16 @@ pub fn iter_into_objs<const N: usize>(iterable: Obj) -> Result<[Obj; N], Error>
}
pub fn iter_into_array<T, const N: usize>(iterable: Obj) -> Result<[T; N], Error>
where
T: TryFrom<Obj, Error = Error>,
{
let err = Error::ValueError(cstr!("Invalid iterable length"));
let vec: Vec<T, N> = iter_into_vec(iterable)?;
// Returns error if array.len() != N
vec.into_array().map_err(|_| err)
}
pub fn iter_into_vec<T, const N: usize>(iterable: Obj) -> Result<Vec<T, N>, Error>
where
T: TryFrom<Obj, Error = Error>,
{
@ -49,8 +60,7 @@ where
for item in Iter::try_from_obj_with_buf(iterable, &mut iter_buf)? {
vec.push(item.try_into()?).map_err(|_| err)?;
}
// Returns error if array.len() != N
vec.into_array().map_err(|_| err)
Ok(vec)
}
/// Maximum number of characters that can be displayed on screen at once. Used
@ -241,6 +251,35 @@ pub extern "C" fn upy_jpeg_info(data: Obj) -> Obj {
unsafe { try_or_raise(block) }
}
#[cfg(feature = "toif")]
pub extern "C" fn upy_toif_info(data: Obj) -> Obj {
let block = || {
let buffer = unsafe { get_buffer(data) };
if let Ok(buffer) = buffer {
let toif = Toif::new(buffer);
if let Some(toif) = toif {
let obj = unsafe {
let values = [
mp_obj_new_int(toif.width() as _),
mp_obj_new_int(toif.height() as _),
];
mp_obj_new_tuple(2, values.as_ptr())
};
Ok(obj)
} else {
Err(Error::ValueError(cstr!("Invalid image format.")))
}
} else {
Err(Error::ValueError(cstr!("Buffer error.")))
}
};
unsafe { try_or_raise(block) }
}
#[cfg(feature = "jpeg")]
pub extern "C" fn upy_jpeg_test(data: Obj) -> Obj {
let block = || {

@ -5,7 +5,6 @@ macro_rules! include_res {
};
}
#[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),+) => {
@ -17,7 +16,6 @@ macro_rules! build_string {
}
}
#[cfg(feature = "ui_debug")]
/// Transforms integer into string slice. For example for printing.
macro_rules! inttostr {
($int:expr) => {{

@ -111,6 +111,7 @@ impl Component for Confirm {
self.right.paint();
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.left.bounds(sink);
self.right.bounds(sink);

@ -107,6 +107,7 @@ impl Component for Intro {
self.menu.paint();
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.title.bounds(sink);
self.text.bounds(sink);

@ -1,25 +1,12 @@
use crate::ui::{
component::{Child, Component, Event, EventCtx, Pad},
component::{Child, Component, Event, EventCtx, Never, Pad},
geometry::{Point, Rect},
model_tr::{
bootloader::{theme::BLD_BG, title::Title, ReturnToC},
bootloader::{theme::BLD_BG, title::Title},
constant::{HEIGHT, WIDTH},
},
};
#[repr(u32)]
#[derive(Copy, Clone)]
pub enum MenuMsg {
Close = 1,
Reboot = 2,
FactoryReset = 3,
}
impl ReturnToC for MenuMsg {
fn return_to_c(self) -> u32 {
self as u32
}
}
pub struct Menu {
bg: Pad,
title: Child<Title>,
@ -37,7 +24,7 @@ impl Menu {
}
impl Component for Menu {
type Msg = MenuMsg;
type Msg = Never;
fn place(&mut self, bounds: Rect) -> Rect {
self.bg

@ -78,7 +78,7 @@ where
{
frame.place(SCREEN_ADJ);
frame.paint();
fade_backlight_duration(BACKLIGHT_NORMAL as _, 500);
fade_backlight_duration(BACKLIGHT_NORMAL, 500);
while button_eval().is_some() {}
@ -139,6 +139,7 @@ extern "C" fn screen_install_confirm(
message.add(Paragraph::new(&theme::TEXT_BOLD, "Seed will be erased!").centered());
}
// TODO: this relies on StrBuffer support for bootloader, decide what to do
let left = Button::with_text(ButtonPos::Left, "CANCEL", bld_button_cancel());
let right = Button::with_text(ButtonPos::Right, "INSTALL", bld_button_default());
@ -172,6 +173,7 @@ extern "C" fn screen_wipe_confirm() -> u32 {
let message =
Paragraphs::new(messages).with_placement(LinearPlacement::vertical().align_at_center());
// TODO: this relies on StrBuffer support for bootloader, decide what to do
let left = Button::with_text(ButtonPos::Left, "WIPE", bld_button_default());
let right = Button::with_text(ButtonPos::Right, "CANCEL", bld_button_cancel());
@ -220,12 +222,17 @@ fn screen_progress(
let fill_to = (loader_area.width() as u32 * progress as u32) / 1000;
display::bar_with_text_and_fill(loader_area, Some(text), fg_color, bg_color, 0, fill_to as _);
display::bar_with_text_and_fill(
loader_area,
Some(&text),
fg_color,
bg_color,
0,
fill_to as _,
);
display::refresh();
}
const INITIAL_INSTALL_LOADER_COLOR: Color = Color::rgb(0x4A, 0x90, 0xE2);
#[no_mangle]
extern "C" fn screen_install_progress(progress: u16, initialize: bool, _initial_setup: bool) {
screen_progress(

@ -2,7 +2,7 @@ use crate::ui::{
component::text::TextStyle,
display::{Color, Font},
model_tr::{
component::{ButtonStyle, ButtonStyleSheet},
component::ButtonStyleSheet,
theme::{BG, BLACK, FG, WHITE},
},
};
@ -10,40 +10,12 @@ use crate::ui::{
pub const BLD_BG: Color = BLACK;
pub const BLD_FG: Color = WHITE;
// Commonly used corner radius (i.e. for buttons).
pub const RADIUS: u8 = 2;
// Size of icons in the UI (i.e. inside buttons).
pub const ICON_SIZE: i32 = 16;
pub fn bld_button_default() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::NORMAL,
text_color: BG,
border_horiz: true,
},
active: &ButtonStyle {
font: Font::NORMAL,
text_color: FG,
border_horiz: true,
},
}
ButtonStyleSheet::new(BG, FG, false, false, None, None)
}
pub fn bld_button_cancel() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::NORMAL,
text_color: FG,
border_horiz: false,
},
active: &ButtonStyle {
font: Font::NORMAL,
text_color: BG,
border_horiz: false,
},
}
ButtonStyleSheet::new(FG, BG, false, false, None, None)
}
pub const TEXT_NORMAL: TextStyle = TextStyle::new(Font::NORMAL, BLD_FG, BLD_BG, BLD_FG, BLD_FG);

@ -48,5 +48,6 @@ impl Component for Title {
);
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, _sink: &mut dyn FnMut(Rect)) {}
}

@ -0,0 +1,270 @@
use heapless::Vec;
use crate::{
error::Error,
micropython::buffer::StrBuffer,
ui::{
component::{
text::paragraphs::{Paragraph, ParagraphSource, ParagraphVecShort, Paragraphs, VecExt},
Child, Component, Event, EventCtx, Pad, Paginate, Qr,
},
geometry::Rect,
model_tr::theme,
},
};
use super::{ButtonController, ButtonControllerMsg, ButtonDetails, ButtonLayout, ButtonPos, Frame};
const MAX_XPUBS: usize = 16;
const QR_BORDER: i16 = 3;
pub enum AddressDetailsMsg {
Cancelled,
}
pub struct AddressDetails {
qr_code: Qr,
details_view: Paragraphs<ParagraphVecShort<StrBuffer>>,
xpub_view: Frame<Paragraphs<Paragraph<StrBuffer>>>,
xpubs: Vec<(StrBuffer, StrBuffer), MAX_XPUBS>,
current_page: usize,
current_subpage: usize,
area: Rect,
pad: Pad,
buttons: Child<ButtonController<StrBuffer>>,
}
impl AddressDetails {
pub fn new(
qr_address: StrBuffer,
case_sensitive: bool,
account: Option<StrBuffer>,
path: Option<StrBuffer>,
) -> Result<Self, Error> {
let qr_code = Qr::new(qr_address, case_sensitive)?.with_border(QR_BORDER);
let details_view = {
let mut para = ParagraphVecShort::new();
if let Some(account) = account {
para.add(Paragraph::new(&theme::TEXT_BOLD, "Account:".into()));
para.add(Paragraph::new(&theme::TEXT_MONO, account));
}
if let Some(path) = path {
para.add(Paragraph::new(&theme::TEXT_BOLD, "Derivation path:".into()));
para.add(Paragraph::new(&theme::TEXT_MONO, path));
}
Paragraphs::new(para)
};
let xpub_view = Frame::new(
"".into(),
Paragraph::new(&theme::TEXT_MONO_DATA, "".into()).into_paragraphs(),
);
let result = Self {
qr_code,
details_view,
xpub_view,
xpubs: Vec::new(),
area: Rect::zero(),
current_page: 0,
current_subpage: 0,
pad: Pad::with_background(theme::BG).with_clear(),
buttons: Child::new(ButtonController::new(ButtonLayout::arrow_none_arrow())),
};
Ok(result)
}
pub fn add_xpub(&mut self, title: StrBuffer, xpub: StrBuffer) -> Result<(), Error> {
self.xpubs
.push((title, xpub))
.map_err(|_| Error::OutOfRange)
}
fn is_in_subpage(&self) -> bool {
self.current_subpage > 0
}
fn is_xpub_page(&self) -> bool {
self.current_page > 1
}
fn is_last_page(&self) -> bool {
self.current_page == self.page_count() - 1
}
fn is_last_subpage(&mut self) -> bool {
self.current_subpage == self.subpages_in_current_page() - 1
}
fn subpages_in_current_page(&mut self) -> usize {
if self.is_xpub_page() {
self.xpub_view.inner_mut().page_count()
} else {
1
}
}
// Button layout for the current page.
// Normally there are arrows everywhere, apart from the right side of the last
// page. On xpub pages there is VIEW FULL middle button when it cannot fit
// one page. On xpub subpages there are wide arrows to scroll.
fn get_button_layout(&mut self) -> ButtonLayout<StrBuffer> {
let (left, middle, right) = if self.is_in_subpage() {
let left = Some(ButtonDetails::up_arrow_icon_wide());
let right = if self.is_last_subpage() {
None
} else {
Some(ButtonDetails::down_arrow_icon_wide())
};
(left, None, right)
} else {
let left = Some(ButtonDetails::left_arrow_icon());
let middle = if self.is_xpub_page() && self.subpages_in_current_page() > 1 {
Some(ButtonDetails::armed_text("VIEW FULL".into()))
} else {
None
};
let right = if self.is_last_page() {
None
} else {
Some(ButtonDetails::right_arrow_icon())
};
(left, middle, right)
};
ButtonLayout::new(left, middle, right)
}
/// Reflecting the current page in the buttons.
fn update_buttons(&mut self, ctx: &mut EventCtx) {
let btn_layout = self.get_button_layout();
self.buttons.mutate(ctx, |_ctx, buttons| {
buttons.set(btn_layout);
});
}
fn page_count(&self) -> usize {
2 + self.xpubs.len()
}
fn fill_xpub_page(&mut self, ctx: &mut EventCtx) {
let i = self.current_page - 2;
self.xpub_view.update_title(ctx, self.xpubs[i].0.clone());
self.xpub_view.update_content(ctx, |p| {
p.inner_mut().update(self.xpubs[i].1.clone());
p.change_page(0)
});
}
fn change_page(&mut self, ctx: &mut EventCtx) {
if self.is_xpub_page() {
self.fill_xpub_page(ctx);
}
self.pad.clear();
self.current_subpage = 0;
}
fn change_subpage(&mut self, ctx: &mut EventCtx) {
if self.is_xpub_page() {
self.xpub_view
.update_content(ctx, |p| p.change_page(self.current_subpage));
self.pad.clear();
}
}
}
impl Component for AddressDetails {
type Msg = AddressDetailsMsg;
fn place(&mut self, bounds: Rect) -> Rect {
// QR code is being placed on the whole bounds, so it can be as big as possible
// (it will not collide with the buttons, they are narrow and on the sides).
// Therefore, also placing pad on the whole bounds.
self.qr_code.place(bounds);
self.pad.place(bounds);
let (content_area, button_area) = bounds.split_bottom(theme::BUTTON_HEIGHT);
self.details_view.place(content_area);
self.xpub_view.place(content_area);
self.buttons.place(button_area);
self.area = content_area;
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
// Possibly update the components that have e.g. marquee
match self.current_page {
0 => self.qr_code.event(ctx, event),
1 => self.details_view.event(ctx, event),
_ => self.xpub_view.event(ctx, event),
};
let button_event = self.buttons.event(ctx, event);
if let Some(ButtonControllerMsg::Triggered(button)) = button_event {
if self.is_in_subpage() {
match button {
ButtonPos::Left => {
// Going back
self.current_subpage -= 1;
}
ButtonPos::Right => {
// Going next
self.current_subpage += 1;
}
_ => unreachable!(),
}
self.change_subpage(ctx);
self.update_buttons(ctx);
return None;
} else {
match button {
ButtonPos::Left => {
// Cancelling or going back
if self.current_page == 0 {
return Some(AddressDetailsMsg::Cancelled);
}
self.current_page -= 1;
self.change_page(ctx);
}
ButtonPos::Right => {
// Going to the next page
self.current_page += 1;
self.change_page(ctx);
}
ButtonPos::Middle => {
// Going into subpage
self.current_subpage = 1;
self.change_subpage(ctx);
}
}
self.update_buttons(ctx);
return None;
}
}
None
}
fn paint(&mut self) {
self.pad.paint();
self.buttons.paint();
match self.current_page {
0 => self.qr_code.paint(),
1 => self.details_view.paint(),
_ => self.xpub_view.paint(),
}
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(self.area)
}
}
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for AddressDetails {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("AddressDetails");
match self.current_page {
0 => t.child("qr_code", &self.qr_code),
1 => t.child("details_view", &self.details_view),
_ => t.child("xpub_view", &self.xpub_view),
}
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,498 @@
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,
},
};
/// All possible states buttons (left and right) can be at.
#[derive(Copy, Clone, PartialEq, Eq)]
enum ButtonState {
/// Both buttons are in untouched state.
/// _ _
/// NEXT: OneDown
Nothing,
/// One Button is down when previously nothing was.
/// _ _ ... ↓ _ | _ ↓
/// NEXT: Nothing, BothDown
OneDown(PhysicalButton),
/// Both buttons are down ("middle-click").
/// ↓ _ | _ ↓ ... ↓ ↓
/// NEXT: OneReleased
BothDown,
/// One button is down when previously both were.
/// Happens when "middle-click" is performed.
/// ↓ ↓ ... ↓ _ | _ ↓
/// NEXT: Nothing, BothDown
OneReleased(PhysicalButton),
/// One button is down after it triggered a HoldToConfirm event.
/// Needed so that we can cleanly reset the state.
/// ↓ _ | _ ↓ ... ↓ _ | _ ↓
/// NEXT: Nothing
HTCNeedsRelease(PhysicalButton),
}
pub enum ButtonControllerMsg {
Pressed(ButtonPos),
Triggered(ButtonPos),
}
/// Defines what kind of button should be currently used.
pub enum ButtonType<T>
where
T: AsRef<str> + Clone + From<&'static str>,
{
Button(Button<T>),
HoldToConfirm(HoldToConfirm<T>),
Nothing,
}
impl<T> ButtonType<T>
where
T: AsRef<str> + Clone + From<&'static str>,
{
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.text.clone() {
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_loader(), duration)
} else {
panic!("ButtonContainer: only text supported for HTC");
}
}
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>
where
T: AsRef<str> + Clone + From<&'static str>,
{
pos: ButtonPos,
button_type: ButtonType<T>,
}
impl<T> ButtonContainer<T>
where
T: AsRef<str> + Clone + From<&'static str>,
{
/// 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>
where
T: AsRef<str> + Clone + From<&'static str>,
{
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> ButtonController<T>
where
T: AsRef<str> + Clone + From<&'static str>,
{
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) {
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> Component for ButtonController<T>
where
T: AsRef<str> + Clone + From<&'static str>,
{
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_event) => {
let (new_state, event) = match self.state {
// _ _
ButtonState::Nothing => match button_event {
// ▼ * | * ▼
ButtonEvent::ButtonPressed(which) => (
// ↓ _ | _ ↓
ButtonState::OneDown(which),
match which {
// ▼ *
PhysicalButton::Left => {
self.left_btn.hold_started(ctx);
Some(ButtonControllerMsg::Pressed(ButtonPos::Left))
}
// * ▼
PhysicalButton::Right => {
self.right_btn.hold_started(ctx);
Some(ButtonControllerMsg::Pressed(ButtonPos::Right))
}
},
),
_ => (self.state, None),
},
// ↓ _ | _ ↓
ButtonState::OneDown(which_down) => match button_event {
// ▲ * | * ▲
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
},
),
},
// * ▼ | ▼ *
ButtonEvent::ButtonPressed(b) if b != which_down => {
self.middle_hold_started(ctx);
(
// ↓ ↓
ButtonState::BothDown,
Some(ButtonControllerMsg::Pressed(ButtonPos::Middle)),
)
}
_ => (self.state, None),
},
// ↓ ↓
ButtonState::BothDown => match button_event {
// ▲ * | * ▲
ButtonEvent::ButtonReleased(b) => {
self.middle_btn.hold_ended(ctx);
// _ ↓ | ↓ _
(ButtonState::OneReleased(b), None)
}
_ => (self.state, None),
},
// ↓ _ | _ ↓
ButtonState::OneReleased(which_up) => match button_event {
// * ▼ | ▼ *
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_event {
// 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
}
// HoldToConfirm expiration
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
}
}
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
use super::ButtonContent;
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for ButtonContainer<T>
where
T: AsRef<str> + Clone + From<&'static str>,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("Button");
if let ButtonType::Button(btn) = &self.button_type {
match btn.content() {
ButtonContent::Text(text) => {
t.string("text", text.as_ref());
}
ButtonContent::Icon(_cursor) => {
t.bool("icon", true);
}
}
} else if let ButtonType::HoldToConfirm(htc) = &self.button_type {
t.string("text", htc.get_text().as_ref());
t.int("htc_ms", htc.get_duration().to_millis() as i64);
}
}
}
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for ButtonController<T>
where
T: AsRef<str> + Clone + From<&'static str>,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("ButtonController");
t.child("left_btn", &self.left_btn);
t.child("middle_btn", &self.middle_btn);
t.child("right_btn", &self.right_btn);
}
}

@ -0,0 +1,149 @@
use crate::ui::{
component::{Component, Event, EventCtx, Never, Pad},
display::Font,
geometry::{Alignment, Point, Rect},
util::long_line_content_with_ellipsis,
};
use super::{common, 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> {
pad: Pad,
text: T,
font: Font,
/// Whether to show the text. Can be disabled.
show_content: bool,
alignment: Alignment,
}
impl<T> ChangingTextLine<T>
where
T: AsRef<str>,
{
pub fn new(text: T, font: Font, alignment: Alignment) -> Self {
Self {
pad: Pad::with_background(theme::BG),
text,
font,
show_content: true,
alignment,
}
}
pub fn center_mono(text: T) -> Self {
Self::new(text, Font::MONO, Alignment::Center)
}
pub fn center_bold(text: T) -> Self {
Self::new(text, Font::BOLD, Alignment::Center)
}
/// Update the text to be displayed in the line.
pub fn update_text(&mut self, text: T) {
self.text = text;
}
/// Get current text.
pub fn get_text(&self) -> &T {
&self.text
}
/// Whether we should display the text content.
/// If not, the whole area (Pad) will still be cleared.
/// Is valid until this function is called again.
pub fn show_or_not(&mut self, show: bool) {
self.show_content = show;
}
/// 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.pad.area.y0 + self.font.line_height()
}
/// Whether the whole text can be painted in the available space
fn text_fits_completely(&self) -> bool {
self.font.text_width(self.text.as_ref()) <= self.pad.area.width()
}
fn paint_left(&self) {
let baseline = Point::new(self.pad.area.x0, self.y_baseline());
common::display(baseline, &self.text, self.font);
}
fn paint_center(&self) {
let baseline = Point::new(self.pad.area.bottom_center().x, self.y_baseline());
common::display_center(baseline, &self.text, self.font);
}
fn paint_right(&self) {
let baseline = Point::new(self.pad.area.x1, self.y_baseline());
common::display_right(baseline, &self.text, self.font);
}
fn paint_long_content_with_ellipsis(&self) {
let text_to_display = long_line_content_with_ellipsis(
self.text.as_ref(),
"...",
self.font,
self.pad.area.width(),
);
// Creating the notion of motion by shifting the text left and right with
// each new text character.
// (So that it is apparent for the user that the text is changing.)
let x_offset = if self.text.as_ref().len() % 2 == 0 {
0
} else {
2
};
let baseline = Point::new(self.pad.area.x0 + x_offset, self.y_baseline());
common::display(baseline, &text_to_display, self.font);
}
}
impl<T> Component for ChangingTextLine<T>
where
T: AsRef<str>,
{
type Msg = Never;
fn place(&mut self, bounds: Rect) -> Rect {
self.pad.place(bounds);
bounds
}
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
None
}
fn paint(&mut self) {
// Always re-painting from scratch.
// Effectively clearing the line completely
// when `self.show_content` is set to `false`.
self.pad.clear();
self.pad.paint();
if self.show_content {
// In the case text cannot fit, show ellipsis and its right part
if !self.text_fits_completely() {
self.paint_long_content_with_ellipsis();
} else {
match self.alignment {
Alignment::Start => self.paint_left(),
Alignment::Center => self.paint_center(),
Alignment::End => self.paint_right(),
}
}
}
}
}

@ -0,0 +1,82 @@
use crate::{
micropython::buffer::StrBuffer,
ui::{
component::{base::Never, Component, Event, EventCtx},
display::{text_multiline_split_words, Font},
geometry::{Alignment, Rect},
model_tr::theme,
},
};
const HEADER: &str = "COINJOIN IN PROGRESS";
const FOOTER: &str = "Don't disconnect your Trezor";
pub struct CoinJoinProgress {
text: StrBuffer,
area: Rect,
}
impl CoinJoinProgress {
pub fn new(text: StrBuffer, _indeterminate: bool) -> Self {
Self {
text,
area: Rect::zero(),
}
}
}
impl Component for CoinJoinProgress {
type Msg = Never;
fn place(&mut self, bounds: Rect) -> Rect {
self.area = bounds;
bounds
}
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
None
}
fn paint(&mut self) {
// Trying to paint all three parts into the area, stopping if any of them
// doesn't fit.
let mut possible_rest = text_multiline_split_words(
self.area,
HEADER,
Font::NORMAL,
theme::FG,
theme::BG,
Alignment::Center,
);
if let Some(rest) = possible_rest {
possible_rest = text_multiline_split_words(
rest,
self.text.as_ref(),
Font::MONO,
theme::FG,
theme::BG,
Alignment::Center,
);
} else {
return;
}
if let Some(rest) = possible_rest {
text_multiline_split_words(
rest,
FOOTER,
Font::BOLD,
theme::FG,
theme::BG,
Alignment::Center,
);
}
}
}
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for CoinJoinProgress {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("CoinJoinProgress");
t.string("text", self.text.as_ref());
}
}

@ -0,0 +1,28 @@
use crate::ui::{
display::{self, Font},
geometry::Point,
};
use super::theme;
/// Display white text on black background
pub fn display<T: AsRef<str>>(baseline: Point, text: &T, font: Font) {
display::text_left(baseline, text.as_ref(), font, theme::FG, theme::BG);
}
/// Display black text on white background
pub fn display_inverse<T: AsRef<str>>(baseline: Point, text: &T, font: Font) {
display::text_left(baseline, text.as_ref(), font, theme::BG, theme::FG);
}
/// 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);
}

@ -1,89 +0,0 @@
use crate::{
time::Instant,
ui::{
component::{Component, Event, EventCtx},
event::ButtonEvent,
geometry::{Point, Rect},
model_tr::component::{loader::Loader, ButtonPos, LoaderMsg, LoaderStyleSheet},
},
};
pub enum HoldToConfirmMsg {
Confirmed,
FailedToConfirm,
}
pub struct HoldToConfirm {
area: Rect,
pos: ButtonPos,
loader: Loader,
baseline: Point,
text_width: i16,
}
impl HoldToConfirm {
pub fn new(pos: ButtonPos, text: &'static str, styles: LoaderStyleSheet) -> 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(),
text_width,
}
}
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,
}
}
}
impl Component for HoldToConfirm {
type Msg = HoldToConfirmMsg;
fn place(&mut self, bounds: Rect) -> Rect {
let loader_area = self.placement(bounds, self.pos);
self.loader.place(loader_area)
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
match event {
Event::Button(ButtonEvent::ButtonPressed(which)) if self.pos.hit(&which) => {
self.loader.start_growing(ctx, Instant::now());
}
Event::Button(ButtonEvent::ButtonReleased(which)) if self.pos.hit(&which) => {
if self.loader.is_animating() {
self.loader.start_shrinking(ctx, Instant::now());
}
}
_ => {}
};
let msg = self.loader.event(ctx, event);
if let Some(LoaderMsg::GrownCompletely) = msg {
return Some(HoldToConfirmMsg::Confirmed);
}
if let Some(LoaderMsg::ShrunkCompletely) = msg {
return Some(HoldToConfirmMsg::FailedToConfirm);
}
None
}
fn paint(&mut self) {
self.loader.paint();
}
}
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for HoldToConfirm {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("HoldToConfirm");
t.child("loader", &self.loader);
}
}

@ -1,93 +0,0 @@
use super::button::{Button, ButtonMsg::Clicked};
use crate::ui::{
component::{Child, Component, Event, EventCtx},
display::Font,
geometry::Rect,
};
pub enum DialogMsg<T> {
Content(T),
LeftClicked,
RightClicked,
}
pub struct Dialog<T, U> {
content: Child<T>,
left_btn: Option<Child<Button<U>>>,
right_btn: Option<Child<Button<U>>>,
}
impl<T, U> Dialog<T, U>
where
T: Component,
U: AsRef<str>,
{
pub fn new(content: T, left: Option<Button<U>>, right: Option<Button<U>>) -> Self {
Self {
content: Child::new(content),
left_btn: left.map(Child::new),
right_btn: right.map(Child::new),
}
}
pub fn inner(&self) -> &T {
self.content.inner()
}
}
impl<T, U> Component for Dialog<T, U>
where
T: Component,
U: AsRef<str>,
{
type Msg = DialogMsg<T::Msg>;
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);
self.content.place(content_area);
self.left_btn.as_mut().map(|b| b.place(button_area));
self.right_btn.as_mut().map(|b| b.place(button_area));
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
if let Some(msg) = self.content.event(ctx, event) {
Some(DialogMsg::Content(msg))
} else if let Some(Clicked) = self.left_btn.as_mut().and_then(|b| b.event(ctx, event)) {
Some(DialogMsg::LeftClicked)
} else if let Some(Clicked) = self.right_btn.as_mut().and_then(|b| b.event(ctx, event)) {
Some(DialogMsg::RightClicked)
} else {
None
}
}
fn paint(&mut self) {
self.content.paint();
if let Some(b) = self.left_btn.as_mut() {
b.paint();
}
if let Some(b) = self.right_btn.as_mut() {
b.paint();
}
}
}
#[cfg(feature = "ui_debug")]
impl<T, U> crate::trace::Trace for Dialog<T, U>
where
T: crate::trace::Trace,
U: crate::trace::Trace + AsRef<str>,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("Dialog");
t.child("content", &self.content);
if let Some(b) = self.left_btn.as_ref() {
t.child("left", b)
}
if let Some(b) = self.right_btn.as_ref() {
t.child("right", b)
}
}
}

@ -0,0 +1,329 @@
use crate::{
micropython::buffer::StrBuffer,
ui::{
component::{Child, Component, ComponentExt, Event, EventCtx, Pad},
geometry::Rect,
model_tr::component::{scrollbar::SCROLLBAR_SPACE, title::Title},
},
};
use super::{
theme, ButtonAction, ButtonController, ButtonControllerMsg, ButtonLayout, ButtonPos, FlowPages,
Page, ScrollBar,
};
/// To be returned directly from Flow.
pub enum FlowMsg {
Confirmed,
ConfirmedIndex(usize),
Cancelled,
Info,
}
pub struct Flow<F, const M: usize> {
/// Function to get pages from
pages: FlowPages<F, M>,
/// Instance of the current Page
current_page: Page<M>,
/// Title being shown at the top in bold
title: Option<Title>,
scrollbar: Child<ScrollBar>,
content_area: Rect,
title_area: Rect,
pad: Pad,
buttons: Child<ButtonController<StrBuffer>>,
page_counter: usize,
return_confirmed_index: bool,
}
impl<F, const M: usize> Flow<F, M>
where
F: Fn(usize) -> Page<M>,
{
pub fn new(pages: FlowPages<F, M>) -> Self {
let current_page = pages.get(0);
Self {
pages,
current_page,
title: None,
content_area: Rect::zero(),
title_area: Rect::zero(),
scrollbar: Child::new(ScrollBar::to_be_filled_later()),
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,
return_confirmed_index: false,
}
}
/// 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.title = Some(Title::new(title));
self
}
/// Causing the Flow to return the index of the page that was confirmed.
pub fn with_return_confirmed_index(mut self) -> Self {
self.return_confirmed_index = true;
self
}
/// Getting new current page according to page counter.
/// Also updating the possible title and moving the scrollbar to correct
/// position.
fn change_current_page(&mut self, ctx: &mut EventCtx) {
self.current_page = self.pages.get(self.page_counter);
if self.title.is_some() {
if let Some(title) = self.current_page.title() {
self.title = Some(Title::new(title));
self.title.place(self.title_area);
}
}
let scrollbar_active_index = self
.pages
.scrollbar_page_index(self.content_area, self.page_counter);
self.scrollbar.mutate(ctx, |_ctx, scrollbar| {
scrollbar.set_active_page(scrollbar_active_index);
});
}
/// Placing current page, setting current buttons and clearing.
fn update(&mut self, ctx: &mut EventCtx, get_new_page: bool) {
if get_new_page {
self.change_current_page(ctx);
}
self.current_page.place(self.content_area);
self.set_buttons(ctx);
self.scrollbar.request_complete_repaint(ctx);
self.clear_and_repaint(ctx);
}
/// Clearing the whole area and requesting repaint.
fn clear_and_repaint(&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() + index as usize;
} else {
self.page_counter = index as usize;
}
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 += jump as usize;
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.
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();
let inner_page = self.current_page.get_current_page();
self.scrollbar.mutate(ctx, |ctx, scrollbar| {
scrollbar.set_active_page(self.page_counter + inner_page);
scrollbar.request_complete_repaint(ctx);
});
self.update(ctx, false);
true
} else if matches!(pos, ButtonPos::Right) && self.current_page.has_next_page() {
self.current_page.go_to_next_page();
let inner_page = self.current_page.get_current_page();
self.scrollbar.mutate(ctx, |ctx, scrollbar| {
scrollbar.set_active_page(self.page_counter + inner_page);
scrollbar.request_complete_repaint(ctx);
});
self.update(ctx, false);
true
} else {
false
}
}
}
impl<F, const M: usize> Component for Flow<F, M>
where
F: Fn(usize) -> 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 (title_area, content_area) = if self.title.is_some() {
title_content_area.split_top(theme::FONT_HEADER.line_height())
} else {
(Rect::zero(), title_content_area)
};
self.content_area = content_area;
// Finding out the total amount of pages in this flow
let complete_page_count = self.pages.scrollbar_page_count(content_area);
self.scrollbar
.inner_mut()
.set_page_count(complete_page_count);
// Placing a title and scrollbar in case the title is there
// (scrollbar will be active - counting pages - even when not placed and
// painted)
if self.title.is_some() {
let (title_area, scrollbar_area) =
title_area.split_right(self.scrollbar.inner().overall_width() + SCROLLBAR_SPACE);
self.title.place(title_area);
self.title_area = title_area;
self.scrollbar.place(scrollbar_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> {
ctx.set_page_count(self.pages.scrollbar_page_count(self.content_area));
self.title.event(ctx, event);
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 => {
if self.return_confirmed_index {
return Some(FlowMsg::ConfirmedIndex(self.page_counter));
} else {
return Some(FlowMsg::Confirmed);
}
}
ButtonAction::Info => return Some(FlowMsg::Info),
ButtonAction::Select => {}
ButtonAction::Action(_) => {}
}
}
};
None
}
fn paint(&mut self) {
self.pad.paint();
// Scrollbars are painted only with a title
if self.title.is_some() {
self.scrollbar.paint();
self.title.paint();
}
self.buttons.paint();
// On purpose painting current page at the end, after buttons,
// because we sometimes (in the case of QR code) need to use the
// whole height of the display for showing the content
// (and painting buttons last would cover the lower part).
self.current_page.paint();
}
}
// DEBUG-ONLY SECTION BELOW
#[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(usize) -> 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.component("Flow");
t.int("flow_page", self.page_counter as i64);
t.int("flow_page_count", self.pages.count() as i64);
self.report_btn_actions(t);
if let Some(title) = &self.title {
t.child("title", title);
}
t.child("scrollbar", &self.scrollbar);
t.child("buttons", &self.buttons);
t.child("flow_page", &self.current_page);
}
}

@ -0,0 +1,355 @@
use crate::{
micropython::buffer::StrBuffer,
ui::{
component::{
text::{
layout::{LayoutFit, LayoutSink, TextNoOp, TextRenderer},
TextStyle,
},
LineBreaking, Paginate, TextLayout,
},
display::{Font, Icon},
geometry::{Alignment, Offset, Rect},
model_tr::theme,
util::ResultExt,
},
};
use heapless::Vec;
use super::{
flow_pages_helpers::{Op, 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: usize,
}
impl<F, const M: usize> FlowPages<F, M>
where
F: Fn(usize) -> Page<M>,
{
pub fn new(get_page: F, page_count: usize) -> Self {
Self {
get_page,
page_count,
}
}
/// Returns a page on demand on a specified index.
pub fn get(&self, page_index: usize) -> Page<M> {
(self.get_page)(page_index)
}
/// Total amount of pages.
pub fn count(&self) -> usize {
self.page_count
}
/// How many scrollable pages are there in the flow
/// (each page can have arbitrary number of "sub-pages").
pub fn scrollbar_page_count(&self, bounds: Rect) -> usize {
self.scrollbar_page_index(bounds, self.page_count)
}
/// Active scrollbar position connected with the beginning of a specific
/// page index.
pub fn scrollbar_page_index(&self, bounds: Rect, page_index: usize) -> usize {
let mut page_count = 0;
for i in 0..page_index {
let mut current_page = self.get(i);
current_page.place(bounds);
page_count += current_page.page_count;
}
page_count
}
}
#[derive(Clone)]
pub struct Page<const M: usize> {
ops: Vec<Op, M>,
text_layout: TextLayout,
btn_layout: ButtonLayout<StrBuffer>,
btn_actions: ButtonActions,
current_page: usize,
page_count: usize,
char_offset: usize,
title: Option<StrBuffer>,
}
// For `layout.rs`
impl<const M: usize> Page<M> {
pub fn new(
btn_layout: ButtonLayout<StrBuffer>,
btn_actions: ButtonActions,
initial_text_font: Font,
) -> Self {
let style = TextStyle::new(
initial_text_font,
theme::FG,
theme::BG,
theme::FG,
theme::FG,
)
.with_ellipsis_icon(
Icon::new(theme::ICON_NEXT_PAGE),
theme::ELLIPSIS_ICON_MARGIN,
)
.with_prev_page_icon(
Icon::new(theme::ICON_PREV_PAGE),
theme::PREV_PAGE_ICON_MARGIN,
);
Self {
ops: Vec::new(),
text_layout: TextLayout::new(style),
btn_layout,
btn_actions,
current_page: 0,
page_count: 1,
char_offset: 0,
title: None,
}
}
pub const fn with_line_breaking(mut self, line_breaking: LineBreaking) -> Self {
self.text_layout.style.line_breaking = line_breaking;
self
}
}
// For `flow.rs`
impl<const M: usize> Page<M> {
/// Adding title.
pub fn with_title(mut self, title: StrBuffer) -> Self {
self.title = Some(title);
self
}
pub fn paint(&mut self) {
self.change_page(self.current_page);
self.layout_content(&mut TextRenderer);
}
pub fn place(&mut self, bounds: Rect) -> Rect {
self.text_layout.bounds = bounds;
self.page_count = self.page_count();
bounds
}
pub fn btn_layout(&self) -> ButtonLayout<StrBuffer> {
// When we are in pagination inside this flow,
// show the up and down arrows on appropriate sides.
let current = self.btn_layout.clone();
// On the last page showing only the narrow arrow, so the right
// button with possibly long text has enough space.
let btn_left = if self.has_prev_page() && !self.has_next_page() {
Some(ButtonDetails::up_arrow_icon())
} else if self.has_prev_page() {
Some(ButtonDetails::up_arrow_icon_wide())
} else {
current.btn_left
};
// Middle button should be shown only on the last page, not to collide
// with the fat right button.
let (btn_middle, btn_right) = if self.has_next_page() {
(None, Some(ButtonDetails::down_arrow_icon_wide()))
} else {
(current.btn_middle, current.btn_right)
};
ButtonLayout::new(btn_left, btn_middle, btn_right)
}
pub fn btn_actions(&self) -> ButtonActions {
self.btn_actions
}
pub fn title(&self) -> Option<StrBuffer> {
self.title.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;
}
pub fn get_current_page(&self) -> usize {
self.current_page
}
}
// 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 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: Alignment) -> Self {
self.with_new_item(Op::Alignment(alignment))
}
}
// For `layout.rs` - aggregating operations
impl<const M: usize> Page<M> {
pub fn text_normal(self, text: StrBuffer) -> Self {
self.font(Font::NORMAL).text(text)
}
pub fn text_mono(self, text: StrBuffer) -> Self {
self.font(Font::MONO).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.text_layout.initial_cursor();
self.text_layout
.layout_ops_new(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);
}
}
}
}
}
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
impl<const M: usize> crate::trace::Trace for Page<M> {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
use crate::ui::component::text::layout::trace::TraceSink;
use core::cell::Cell;
let fit: Cell<Option<LayoutFit>> = Cell::new(None);
t.component("Page");
if let Some(title) = &self.title {
// Not calling it "title" as that is already traced by FlowPage
t.string("page_title", title);
}
t.int("active_page", self.current_page as i64);
t.int("page_count", self.page_count as i64);
t.in_list("text", &|l| {
let result = self.layout_content(&mut TraceSink(l));
fit.set(Some(result));
});
}
}

@ -0,0 +1,171 @@
//! Mostly extending TextLayout from ui/component/text/layout.rs
//! (support for more Ops like alignments or arbitrary offsets)
use crate::{
micropython::buffer::StrBuffer,
ui::{
component::{
text::layout::{LayoutFit, LayoutSink},
TextLayout,
},
display::{Color, Font},
geometry::{Alignment, Offset, Point},
},
};
use heapless::Vec;
/// To account for operations that are not made of characters
/// but need to be accounted for somehow.
/// Number of processed characters will be increased by this
/// to account for the operation.
const PROCESSED_CHARS_ONE: usize = 1;
/// Container for text allowing for its displaying by chunks
/// without the need to allocate a new String each time.
#[derive(Clone)]
pub struct ToDisplay {
pub text: StrBuffer,
pub length_from_end: usize,
}
impl ToDisplay {
pub fn new(text: StrBuffer) -> Self {
Self {
text: text.clone(),
length_from_end: text.len(),
}
}
}
/// Operations that can be done on the screen.
#[derive(Clone)]
pub enum Op {
/// Render text with current color and font.
Text(ToDisplay),
/// Set current text color.
Color(Color),
/// Set currently used font.
Font(Font),
/// Set currently used line alignment.
Alignment(Alignment),
/// Move the current cursor by specified Offset.
CursorOffset(Offset),
/// Force continuing on the next page.
NextPage,
}
impl TextLayout {
/// 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_new<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;
// TODO: get cursor at the very top of the page and only then subtract
// the font height according to the CURRENT font - which can be different
// from the original font.
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_from_end);
if skipped > skip_bytes {
let leave_bytes = skipped - skip_bytes;
let new_display = ToDisplay {
text: to_display.text,
length_from_end: leave_bytes,
};
Some(Op::Text(new_display))
} else {
None
}
}
Op::NextPage if skipped < skip_bytes => {
skipped = skipped.saturating_add(PROCESSED_CHARS_ONE);
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::Alignment(line_alignment) => {
self.align = 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
total_processed_chars += PROCESSED_CHARS_ONE;
return LayoutFit::OutOfBounds {
processed_chars: total_processed_chars,
height: self.layout_height(init_cursor, *cursor),
};
}
// Drawing text or icon
Op::Text(to_display) => {
// Try to fit text on the current page and if they do not fit,
// return the appropriate OutOfBounds message
// TODO: document this a little bit
let text = to_display.text.as_ref();
let text_len = to_display.length_from_end;
let start = text.len() - text_len;
let to_really_display = &text[start..];
let fit = self.layout_text(to_really_display, cursor, sink);
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),
}
}
}

@ -1,78 +1,214 @@
use super::theme;
use crate::ui::{
component::{Child, Component, Event, EventCtx},
display::{self, Font},
geometry::{Insets, Offset, Rect},
use super::{theme, ScrollBar};
use crate::{
micropython::buffer::StrBuffer,
ui::{
component::{Child, Component, ComponentExt, Event, EventCtx},
geometry::{Insets, Rect},
model_tr::{
component::{scrollbar::SCROLLBAR_SPACE, title::Title},
constant,
},
},
};
pub struct Frame<T, U> {
area: Rect,
title: U,
/// Component for holding another component and displaying a title.
pub struct Frame<T> {
title: Title,
content: Child<T>,
}
impl<T, U> Frame<T, U>
impl<T> Frame<T>
where
T: Component,
U: AsRef<str>,
{
pub fn new(title: U, content: T) -> Self {
pub fn new(title: StrBuffer, content: T) -> Self {
Self {
title,
area: Rect::zero(),
title: Title::new(title),
content: Child::new(content),
}
}
/// Aligning the title to the center, instead of the left.
pub fn with_title_centered(mut self) -> Self {
self.title = self.title.with_centered();
self
}
pub fn inner(&self) -> &T {
self.content.inner()
}
pub fn inner_mut(&mut self) -> &mut T {
self.content.inner_mut()
}
pub fn update_title(&mut self, ctx: &mut EventCtx, new_title: StrBuffer) {
self.title.set_text(ctx, new_title);
}
pub fn update_content<F, R>(&mut self, ctx: &mut EventCtx, update_fn: F) -> R
where
F: Fn(&mut T) -> R,
{
self.content.mutate(ctx, |ctx, c| {
let res = update_fn(c);
c.request_complete_repaint(ctx);
res
})
}
}
impl<T, U> Component for Frame<T, U>
impl<T> Component for Frame<T>
where
T: Component,
U: AsRef<str>,
{
type Msg = T::Msg;
fn place(&mut self, bounds: Rect) -> Rect {
const TITLE_SPACE: i16 = 4;
const TITLE_SPACE: i16 = 2;
let (title_area, content_area) = bounds.split_top(Font::BOLD.line_height());
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.title.place(title_area);
self.content.place(content_area);
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
self.title.event(ctx, event);
self.content.event(ctx, event)
}
fn paint(&mut self) {
display::text_left(
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);
self.title.paint();
self.content.paint();
}
}
pub trait ScrollableContent {
fn page_count(&self) -> usize;
fn active_page(&self) -> usize;
}
/// Component for holding another component and displaying a title.
/// Also is allocating space for a scrollbar.
pub struct ScrollableFrame<T> {
title: Option<Child<Title>>,
scrollbar: ScrollBar,
content: Child<T>,
}
impl<T> ScrollableFrame<T>
where
T: Component + ScrollableContent,
{
pub fn new(content: T) -> Self {
Self {
title: None,
scrollbar: ScrollBar::to_be_filled_later(),
content: Child::new(content),
}
}
pub fn inner(&self) -> &T {
self.content.inner()
}
pub fn with_title(mut self, title: StrBuffer) -> Self {
self.title = Some(Child::new(Title::new(title)));
self
}
}
impl<T> Component for ScrollableFrame<T>
where
T: Component + ScrollableContent,
{
type Msg = T::Msg;
fn place(&mut self, bounds: Rect) -> Rect {
// Depending whether there is a title or not
let (content_area, scrollbar_area, title_area) = if self.title.is_none() {
// When the content fits on one page, no need for allocating place for scrollbar
self.content.place(bounds);
if self.content.inner().page_count() == 1 {
(bounds, Rect::zero(), Rect::zero())
} else {
let (scrollbar_area, content_area) =
bounds.split_top(ScrollBar::MAX_DOT_SIZE + constant::LINE_SPACE);
(content_area, scrollbar_area, Rect::zero())
}
} else {
const TITLE_SPACE: i16 = 2;
let (title_and_scrollbar_area, content_area) =
bounds.split_top(theme::FONT_HEADER.line_height());
let content_area = content_area.inset(Insets::top(TITLE_SPACE));
// When there is only one page, do not allocate anything for scrollbar,
// which would reduce the space for title
self.content.place(content_area);
let (title_area, scrollbar_area) = if self.content.inner().page_count() == 1 {
(title_and_scrollbar_area, Rect::zero())
} else {
title_and_scrollbar_area
.split_right(self.scrollbar.overall_width() + SCROLLBAR_SPACE)
};
(content_area, scrollbar_area, title_area)
};
self.content.place(content_area);
self.scrollbar
.set_page_count(self.content.inner().page_count());
self.scrollbar.place(scrollbar_area);
self.title.place(title_area);
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
let msg = self.content.event(ctx, event);
self.scrollbar
.set_active_page(self.content.inner().active_page());
self.scrollbar.request_complete_repaint(ctx);
self.title.event(ctx, event);
self.scrollbar.event(ctx, event);
msg
}
fn paint(&mut self) {
self.title.paint();
self.scrollbar.paint();
self.content.paint();
}
}
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
impl<T, U> crate::trace::Trace for Frame<T, U>
impl<T> crate::trace::Trace for Frame<T>
where
T: crate::trace::Trace,
U: AsRef<str>,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("Frame");
t.string("title", self.title.as_ref());
t.child("title", &self.title);
t.child("content", &self.content);
}
}
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for ScrollableFrame<T>
where
T: crate::trace::Trace,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("ScrollableFrame");
if let Some(title) = &self.title {
t.child("title", title);
}
t.child("scrollbar", &self.scrollbar);
t.child("content", &self.content);
}
}

@ -0,0 +1,126 @@
use crate::{
time::{Duration, Instant},
ui::{
component::{Component, Event, EventCtx},
event::ButtonEvent,
geometry::Rect,
model_tr::{
component::{loader::Loader, ButtonPos, LoaderMsg, LoaderStyleSheet},
theme,
},
},
};
pub enum HoldToConfirmMsg {
Confirmed,
FailedToConfirm,
}
pub struct HoldToConfirm<T>
where
T: AsRef<str> + Clone + From<&'static str>,
{
pos: ButtonPos,
loader: Loader<T>,
text_width: i16,
}
impl<T> HoldToConfirm<T>
where
T: AsRef<str> + Clone + From<&'static str>,
{
pub fn text(pos: ButtonPos, text: T, styles: LoaderStyleSheet, duration: Duration) -> Self {
let text_width = styles.normal.font.visible_text_width(text.as_ref());
Self {
pos,
loader: Loader::text(text, 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);
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 + 2 * theme::BUTTON_OUTLINE;
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<T> Component for HoldToConfirm<T>
where
T: AsRef<str> + Clone + From<&'static str>,
{
type Msg = HoldToConfirmMsg;
fn place(&mut self, bounds: Rect) -> Rect {
let loader_area = self.placement(bounds, self.pos);
self.loader.place(loader_area)
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
match event {
Event::Button(ButtonEvent::HoldStarted) => {
self.loader.start_growing(ctx, Instant::now());
}
Event::Button(ButtonEvent::HoldEnded) => {
if self.loader.is_animating() {
self.loader.start_shrinking(ctx, Instant::now());
}
}
_ => {}
};
let msg = self.loader.event(ctx, event);
if let Some(LoaderMsg::GrownCompletely) = msg {
return Some(HoldToConfirmMsg::Confirmed);
}
if let Some(LoaderMsg::ShrunkCompletely) = msg {
return Some(HoldToConfirmMsg::FailedToConfirm);
}
None
}
fn paint(&mut self) {
self.loader.paint();
}
}
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for HoldToConfirm<T>
where
T: AsRef<str> + Clone + From<&'static str>,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("HoldToConfirm");
t.child("loader", &self.loader);
}
}

@ -0,0 +1,233 @@
use crate::{
micropython::{buffer::StrBuffer, gc::Gc},
storage::{get_avatar, get_avatar_len},
trezorhal::usb::usb_configured,
ui::{
component::{Child, Component, Event, EventCtx, Pad},
display::{
fill_background_for_text, rect_fill,
toif::{icon_from_toif, Toif},
Font, Icon,
},
event::USBEvent,
geometry::{self, Alignment, Offset, Point, Rect},
model_tr::constant,
},
};
use super::{common::display_center, theme, ButtonController, ButtonControllerMsg, ButtonLayout};
const AREA: Rect = constant::screen();
const TOP_CENTER: Point = AREA.top_center();
const LABEL_Y: i16 = constant::HEIGHT - 5;
const LOCKED_INSTRUCTION_Y: i16 = 34;
const LOGO_ICON_TOP_MARGIN: i16 = 12;
const LOCK_ICON_TOP_MARGIN: i16 = 12;
const NOTIFICATION_HEIGHT: i16 = 12;
pub struct Homescreen {
label: StrBuffer,
notification: Option<(StrBuffer, u8)>,
pad: Pad,
/// Used for HTC functionality to lock device from homescreen
invisible_buttons: Child<ButtonController<StrBuffer>>,
}
pub enum HomescreenMsg {
Dismissed,
}
impl Homescreen {
pub fn new(label: StrBuffer, notification: Option<(StrBuffer, u8)>) -> Self {
// NOTE: for some reason the text cannot be empty string, it was panicking at
// library/core/src/str/mod.rs:107
let invisible_btn_layout = ButtonLayout::htc_none_htc("_".into(), "_".into());
Self {
label,
notification,
pad: Pad::with_background(theme::BG),
invisible_buttons: Child::new(ButtonController::new(invisible_btn_layout)),
}
}
fn paint_notification(&self) {
// Filling the background to be well visible even on homescreen image
let baseline = TOP_CENTER + Offset::y(Font::MONO.line_height());
if !usb_configured() {
rect_fill(AREA.split_top(NOTIFICATION_HEIGHT).0, theme::BG);
// TODO: fill warning icons here as well?
display_center(baseline, &"NO USB CONNECTION", Font::MONO);
} else if let Some((notification, _level)) = &self.notification {
rect_fill(AREA.split_top(NOTIFICATION_HEIGHT).0, theme::BG);
display_center(baseline, &notification.as_ref(), Font::MONO);
// TODO: what if the notification text is so long it collides with icons?
let warning_icon = Icon::new(theme::ICON_WARNING);
warning_icon.draw(AREA.top_left(), geometry::TOP_LEFT, theme::FG, theme::BG);
// Needs x+1 Offset to compensate for empty right column (icon needs to be
// even-wide)
warning_icon.draw(
AREA.top_right() + Offset::x(1),
geometry::TOP_RIGHT,
theme::FG,
theme::BG,
);
}
}
fn paint_label(&self) {
let label = self.label.as_ref();
let baseline = TOP_CENTER + Offset::y(LABEL_Y);
fill_background_for_text(
baseline,
label,
Font::NORMAL,
theme::BG,
Alignment::Center,
3,
);
display_center(baseline, &label, Font::NORMAL);
}
fn event_usb(&mut self, ctx: &mut EventCtx, event: Event) {
if let Event::USB(USBEvent::Connected(_)) = event {
ctx.request_paint();
}
}
}
impl Component for Homescreen {
type Msg = HomescreenMsg;
fn place(&mut self, bounds: Rect) -> Rect {
self.pad.place(AREA);
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
Self::event_usb(self, ctx, event);
// HTC press of any button will lock the device
if let Some(ButtonControllerMsg::Triggered(_)) = self.invisible_buttons.event(ctx, event) {
return Some(HomescreenMsg::Dismissed);
}
None
}
fn paint(&mut self) {
self.pad.paint();
// Painting the homescreen image first, as the notification and label
// should be "on top of it"
if let Ok(user_custom_image) = get_user_custom_image() {
// TODO: how to make it better? I did not want to introduce lifetime to
// `Icon`, it would then need to be specified in a lot of places.
let toif_data = unwrap!(Toif::new(user_custom_image.as_ref()));
let r = Rect::snap(TOP_CENTER, toif_data.size(), geometry::TOP_CENTER);
icon_from_toif(&toif_data, r.center(), theme::FG, theme::BG);
} else {
Icon::new(theme::ICON_LOGO).draw(
TOP_CENTER + Offset::y(LOGO_ICON_TOP_MARGIN),
geometry::TOP_CENTER,
theme::FG,
theme::BG,
);
}
self.paint_notification();
self.paint_label();
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(self.pad.area);
}
}
// TODO: this is copy-paste from `model_tt/component/homescreen/mod.rs`,
// probably could be moved to some common place (maybe storage)
fn get_user_custom_image() -> Result<Gc<[u8]>, ()> {
if let Ok(len) = get_avatar_len() {
let result = Gc::<[u8]>::new_slice(len);
if let Ok(mut buffer) = result {
let buf = unsafe { Gc::<[u8]>::as_mut(&mut buffer) };
if get_avatar(buf).is_ok() {
return Ok(buffer);
}
}
};
Err(())
}
pub struct Lockscreen {
label: StrBuffer,
bootscreen: bool,
/// Used for unlocking the device from lockscreen
invisible_buttons: Child<ButtonController<StrBuffer>>,
}
impl Lockscreen {
pub fn new(label: StrBuffer, bootscreen: bool) -> Self {
let invisible_btn_layout = ButtonLayout::text_none_text("_".into(), "_".into());
Lockscreen {
label,
bootscreen,
invisible_buttons: Child::new(ButtonController::new(invisible_btn_layout)),
}
}
}
impl Component for Lockscreen {
type Msg = HomescreenMsg;
fn place(&mut self, bounds: Rect) -> Rect {
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
// Press of any button will unlock the device
if let Some(ButtonControllerMsg::Triggered(_)) = self.invisible_buttons.event(ctx, event) {
return Some(HomescreenMsg::Dismissed);
}
None
}
fn paint(&mut self) {
let instruction = if self.bootscreen {
"Click to Connect"
} else {
"Click to Unlock"
};
display_center(
TOP_CENTER + Offset::y(LOCKED_INSTRUCTION_Y),
&instruction,
Font::MONO,
);
Icon::new(theme::ICON_LOCK).draw(
TOP_CENTER + Offset::y(LOCK_ICON_TOP_MARGIN),
geometry::TOP_CENTER,
theme::FG,
theme::BG,
);
display_center(
TOP_CENTER + Offset::y(LABEL_Y),
&self.label.as_ref(),
Font::NORMAL,
);
}
}
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for Homescreen {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("Homescreen");
t.string("label", self.label.as_ref());
}
}
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for Lockscreen {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("Lockscreen");
t.string("label", self.label.as_ref());
}
}

@ -0,0 +1,459 @@
#[cfg(feature = "ui_debug")]
use crate::trace::Trace;
use crate::ui::{
component::{Child, Component, Event, EventCtx, Pad},
geometry::Rect,
};
use super::super::{theme, ButtonController, ButtonControllerMsg, ButtonLayout, ButtonPos};
pub enum ChoicePageMsg {
Choice(usize),
LeftMost,
RightMost,
}
const DEFAULT_ITEMS_DISTANCE: i16 = 10;
const DEFAULT_Y_BASELINE: i16 = 20;
pub trait Choice<T>
where
T: AsRef<str> + Clone + From<&'static str>,
{
fn paint_center(&self, area: Rect, inverse: bool);
fn width_center(&self) -> i16 {
0
}
fn paint_left(&self, _area: Rect, _show_incomplete: bool) -> Option<i16> {
None
}
fn paint_right(&self, _area: Rect, _show_incomplete: bool) -> Option<i16> {
None
}
fn btn_layout(&self) -> ButtonLayout<T> {
ButtonLayout::default_three_icons()
}
}
/// 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
/// items in `heapless::Vec` (which caused StackOverflow),
/// but offers a "lazy-loading" way of requesting the
/// items only when they are needed, one-by-one.
/// This way, no more than one item is stored in memory at any time.
pub trait ChoiceFactory<T>
where
T: AsRef<str> + Clone + From<&'static str>,
{
#[cfg(feature = "ui_debug")]
type Item: Choice<T> + Trace;
#[cfg(not(feature = "ui_debug"))]
type Item: Choice<T>;
fn count(&self) -> usize;
fn get(&self, index: usize) -> Self::Item;
}
/// 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 `Choice`s (through `ChoiceFactory`)
/// and will receive back the index of the selected choice.
///
/// Each `Choice` 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, T>
where
F: ChoiceFactory<T>,
T: AsRef<str> + Clone + From<&'static str>,
{
choices: F,
pad: Pad,
buttons: Child<ButtonController<T>>,
page_counter: usize,
/// How many pixels from top should we render the items.
y_baseline: i16,
/// How many pixels are between the items.
items_distance: i16,
/// Whether the choice page is "infinite" (carousel).
is_carousel: bool,
/// Whether we should show items on left/right even when they cannot
/// be painted entirely (they would be cut off).
show_incomplete: bool,
/// Whether to show only the currently selected item, nothing left/right.
show_only_one_item: bool,
/// Whether the middle selected item should be painted with
/// inverse colors - black on white.
inverse_selected_item: bool,
}
impl<F, T> ChoicePage<F, T>
where
F: ChoiceFactory<T>,
T: AsRef<str> + Clone + From<&'static str>,
{
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,
y_baseline: DEFAULT_Y_BASELINE,
items_distance: DEFAULT_ITEMS_DISTANCE,
is_carousel: false,
show_incomplete: false,
show_only_one_item: false,
inverse_selected_item: false,
}
}
/// Set the page counter at the very beginning.
/// Need to update the initial button layout.
pub fn with_initial_page_counter(mut self, page_counter: usize) -> Self {
self.page_counter = page_counter;
let initial_btn_layout = self.choices.get(page_counter).btn_layout();
self.buttons = Child::new(ButtonController::new(initial_btn_layout));
self
}
/// Enabling the carousel mode.
pub fn with_carousel(mut self, carousel: bool) -> Self {
self.is_carousel = carousel;
self
}
/// Show incomplete items, even when they cannot render in their entirety.
pub fn with_incomplete(mut self, show_incomplete: bool) -> Self {
self.show_incomplete = show_incomplete;
self
}
/// Show only the currently selected item, nothing left/right.
pub fn with_only_one_item(mut self, only_one_item: bool) -> Self {
self.show_only_one_item = only_one_item;
self
}
/// Adjust the horizontal baseline from the top of placement.
pub fn with_y_baseline(mut self, y_baseline: i16) -> Self {
self.y_baseline = y_baseline;
self
}
/// Adjust the distance between the items.
pub fn with_items_distance(mut self, items_distance: i16) -> Self {
self.items_distance = items_distance;
self
}
/// Resetting the component, which enables reusing the same instance
/// for multiple choice categories.
///
/// Used for example in passphrase, where there are multiple categories of
/// characters.
pub fn reset(
&mut self,
ctx: &mut EventCtx,
new_choices: F,
new_page_counter: Option<usize>,
is_carousel: bool,
) {
self.choices = new_choices;
if let Some(new_counter) = new_page_counter {
self.page_counter = new_counter;
}
self.is_carousel = is_carousel;
self.update(ctx);
}
/// Navigating to the chosen page index.
pub fn set_page_counter(&mut self, ctx: &mut EventCtx, page_counter: usize) {
self.page_counter = page_counter;
self.update(ctx);
}
/// Display current, previous and next choices according to
/// the current ChoiceItem.
fn paint_choices(&mut self) {
let available_area = self.pad.area.split_top(self.y_baseline).0;
// Drawing the current item in the middle.
self.show_current_choice(available_area);
// Not drawing the rest when not wanted
if self.show_only_one_item {
return;
}
// Getting the remaining left and right areas.
let center_width = self.choices.get(self.page_counter).width_center();
let (left_area, _center_area, right_area) = available_area.split_center(center_width);
// Possibly drawing on the left side.
if self.has_previous_choice() || self.is_carousel {
self.show_left_choices(left_area);
}
// Possibly drawing on the right side.
if self.has_next_choice() || self.is_carousel {
self.show_right_choices(right_area);
}
}
/// Setting current buttons, and clearing.
fn update(&mut self, ctx: &mut EventCtx) {
self.set_buttons(ctx);
self.clear_and_repaint(ctx);
}
/// Clearing the whole area and requesting repaint.
fn clear_and_repaint(&mut self, ctx: &mut EventCtx) {
self.pad.clear();
ctx.request_paint();
}
/// Index of the last page.
fn last_page_index(&self) -> usize {
self.choices.count() - 1
}
/// Whether there is a previous choice (on the left).
pub fn has_previous_choice(&self) -> bool {
self.page_counter > 0
}
/// Whether there is a next choice (on the right).
pub fn has_next_choice(&self) -> bool {
self.page_counter < self.last_page_index()
}
/// Display the current choice in the middle.
fn show_current_choice(&mut self, area: Rect) {
self.choices
.get(self.page_counter)
.paint_center(area, self.inverse_selected_item);
// Color inversion is just one-time thing.
if self.inverse_selected_item {
self.inverse_selected_item = false;
}
}
/// Display all the choices fitting on the left side.
/// Going as far as possible.
fn show_left_choices(&self, area: Rect) {
// page index can get negative here, so having it as i16 instead of usize
let mut page_index = self.page_counter as i16 - 1;
let mut x_offset = 0;
loop {
// Breaking out of the loop if we exhausted left items
// and the carousel mode is not enabled.
if page_index < 0 {
if self.is_carousel {
// Moving to the last page.
page_index = self.last_page_index() as i16;
} else {
break;
}
}
let current_area = area.split_right(x_offset + self.items_distance).0;
if let Some(width) = self
.choices
.get(page_index as usize)
.paint_left(current_area, self.show_incomplete)
{
// Updating loop variables.
x_offset += width + self.items_distance;
page_index -= 1;
} else {
break;
}
}
}
/// Display all the choices fitting on the right side.
/// Going as far as possible.
fn show_right_choices(&self, area: Rect) {
let mut page_index = self.page_counter + 1;
let mut x_offset = 3; // starts with a little offset to account for the middle highlight
loop {
// Breaking out of the loop if we exhausted right items
// and the carousel mode is not enabled.
if page_index > self.last_page_index() {
if self.is_carousel {
// Moving to the first page.
page_index = 0;
} else {
break;
}
}
let current_area = area.split_left(x_offset + self.items_distance).1;
if let Some(width) = self
.choices
.get(page_index)
.paint_right(current_area, self.show_incomplete)
{
// Updating loop variables.
x_offset += width + self.items_distance;
page_index += 1;
} else {
break;
}
}
}
/// Decrease the page counter to the previous page.
fn decrease_page_counter(&mut self) {
self.page_counter -= 1;
}
/// Advance page counter to the next page.
fn increase_page_counter(&mut self) {
self.page_counter += 1;
}
/// Set page to the first one.
fn page_counter_to_zero(&mut self) {
self.page_counter = 0;
}
/// Set page to the last one.
fn page_counter_to_max(&mut self) {
self.page_counter = self.last_page_index();
}
/// Get current page counter.
pub fn page_index(&self) -> usize {
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.
fn set_buttons(&mut self, ctx: &mut EventCtx) {
let btn_layout = self.choices.get(self.page_counter).btn_layout();
self.buttons.mutate(ctx, |_ctx, buttons| {
buttons.set(btn_layout);
});
}
}
impl<F, T> Component for ChoicePage<F, T>
where
F: ChoiceFactory<T>,
T: AsRef<str> + Clone + From<&'static str>,
{
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);
// Button was "triggered" - released. Doing the appropriate action.
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_and_repaint(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_and_repaint(ctx);
return Some(ChoicePageMsg::RightMost);
}
}
ButtonPos::Middle => {
// Clicked SELECT. Send current choice index
self.clear_and_repaint(ctx);
return Some(ChoicePageMsg::Choice(self.page_counter));
}
}
};
// The middle button was "pressed", highlighting the current choice by color
// inversion.
if let Some(ButtonControllerMsg::Pressed(ButtonPos::Middle)) = button_event {
self.inverse_selected_item = true;
self.clear_and_repaint(ctx);
};
None
}
fn paint(&mut self) {
self.pad.paint();
self.buttons.paint();
self.paint_choices();
}
}
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
impl<F, T> crate::trace::Trace for ChoicePage<F, T>
where
F: ChoiceFactory<T>,
T: AsRef<str> + Clone + From<&'static str>,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("ChoicePage");
t.int("active_page", self.page_counter as i64);
t.int("page_count", self.choices.count() as i64);
t.bool("is_carousel", self.is_carousel);
if self.has_previous_choice() {
t.child("prev_choice", &self.choices.get(self.page_counter - 1));
} else if self.is_carousel {
// In case of carousel going to the left end.
t.child("prev_choice", &self.choices.get(self.last_page_index()));
}
t.child("current_choice", &self.choices.get(self.page_counter));
if self.has_next_choice() {
t.child("next_choice", &self.choices.get(self.page_counter + 1));
} else if self.is_carousel {
// In case of carousel going to the very left.
t.child("next_choice", &self.choices.get(0));
}
t.child("buttons", &self.buttons);
}
}

@ -0,0 +1,257 @@
use crate::ui::{
display::{rect_fill, rect_fill_corners, rect_outline_rounded, Font, Icon},
geometry::{Offset, Rect, BOTTOM_LEFT, BOTTOM_RIGHT},
model_tr::theme,
};
use heapless::String;
use super::super::{
common::{display, display_inverse, display_right},
ButtonDetails, ButtonLayout, Choice,
};
const ICON_RIGHT_PADDING: i16 = 2;
/// Simple string component used as a choice item.
#[derive(Clone)]
pub struct ChoiceItem<T>
where
T: AsRef<str> + Clone + From<&'static str>,
{
text: String<50>,
icon: Option<Icon>,
btn_layout: ButtonLayout<T>,
font: Font,
}
impl<T> ChoiceItem<T>
where
T: AsRef<str> + Clone + From<&'static str>,
{
pub fn new<F>(text: F, btn_layout: ButtonLayout<T>) -> Self
where
F: AsRef<str>,
{
Self {
text: String::from(text.as_ref()),
icon: None,
btn_layout,
font: theme::FONT_CHOICE_ITEMS,
}
}
/// Allows to add the icon.
pub fn with_icon(mut self, icon: Icon) -> Self {
self.icon = Some(icon);
self
}
/// Allows to change the font.
pub fn with_font(mut self, font: Font) -> Self {
self.font = font;
self
}
/// Getting the offset of the icon to center it vertically.
/// Depending on its size and used font.
fn icon_vertical_offset(&self) -> Offset {
if let Some(icon) = self.icon {
let height_diff = self.font.text_height() - icon.toif.height();
Offset::y(-height_diff / 2)
} else {
Offset::zero()
}
}
/// Getting the visible text width in pixels.
fn visible_text_width(&self) -> i16 {
self.font.visible_text_width(&self.text)
}
/// Getting the initial x-bearing of the text in pixels,
/// so that we can adjust its positioning to center it properly.
fn text_x_bearing(&self) -> i16 {
self.font.start_x_bearing(&self.text)
}
/// Getting the non-central width in pixels.
/// It will show an icon if defined, otherwise the text, not both.
fn width_side(&self) -> i16 {
if let Some(icon) = self.icon {
icon.toif.width()
} else {
self.visible_text_width()
}
}
/// Whether the whole item fits into the given rectangle.
fn fits(&self, rect: Rect) -> bool {
self.width_side() <= rect.width()
}
/// Draws highlight around this choice item.
/// Must be called before the item is drawn, otherwise it will
/// cover the item.
fn paint_rounded_highlight(&self, area: Rect, inverse: bool) {
let bound = theme::BUTTON_OUTLINE;
let left_bottom =
area.bottom_center() + Offset::new(-self.width_center() / 2 - bound, bound + 1);
let x_size = self.width_center() + 2 * bound;
let y_size = self.font.text_height() + 2 * bound;
let outline_size = Offset::new(x_size, y_size);
let outline = Rect::from_bottom_left_and_size(left_bottom, outline_size);
if inverse {
rect_fill(outline, theme::FG);
rect_fill_corners(outline, theme::BG);
} else {
rect_outline_rounded(outline, theme::FG, theme::BG, 1);
}
}
/// Painting the item as a choice on the left side from center.
/// Showing only the icon, if available, otherwise the text.
fn render_left(&self, area: Rect) {
if let Some(icon) = self.icon {
icon.draw(
area.bottom_right() + self.icon_vertical_offset(),
BOTTOM_RIGHT,
theme::FG,
theme::BG,
);
} else {
display_right(area.bottom_right(), &self.text, self.font);
}
}
/// Painting the item as a choice on the right side from center.
/// Showing only the icon, if available, otherwise the text.
fn render_right(&self, area: Rect) {
if let Some(icon) = self.icon {
icon.draw(
area.bottom_left() + self.icon_vertical_offset(),
BOTTOM_LEFT,
theme::FG,
theme::BG,
);
} else {
display(area.bottom_left(), &self.text, self.font);
}
}
/// Setting left button.
pub fn set_left_btn(&mut self, btn_left: Option<ButtonDetails<T>>) {
self.btn_layout.btn_left = btn_left;
}
/// Setting middle button.
pub fn set_middle_btn(&mut self, btn_middle: Option<ButtonDetails<T>>) {
self.btn_layout.btn_middle = btn_middle;
}
/// Setting right button.
pub fn set_right_btn(&mut self, btn_right: Option<ButtonDetails<T>>) {
self.btn_layout.btn_right = btn_right;
}
/// Changing the text.
pub fn set_text(&mut self, text: String<50>) {
self.text = text;
}
}
impl<T> Choice<T> for ChoiceItem<T>
where
T: AsRef<str> + Clone + From<&'static str>,
{
/// Painting the item as the main choice in the middle.
/// Showing both the icon and text, if the icon is available.
fn paint_center(&self, area: Rect, inverse: bool) {
self.paint_rounded_highlight(area, inverse);
let mut baseline = area.bottom_center() + Offset::x(-self.width_center() / 2);
if let Some(icon) = self.icon {
let fg_color = if inverse { theme::BG } else { theme::FG };
let bg_color = if inverse { theme::FG } else { theme::BG };
icon.draw(
baseline + self.icon_vertical_offset(),
BOTTOM_LEFT,
fg_color,
bg_color,
);
baseline = baseline + Offset::x(icon.toif.width() + ICON_RIGHT_PADDING);
}
// Possibly shifting the baseline left, when there is a text bearing.
// This is to center the text properly.
baseline = baseline - Offset::x(self.text_x_bearing());
if inverse {
display_inverse(baseline, &self.text, self.font);
} else {
display(baseline, &self.text, self.font);
}
}
/// Getting the overall width in pixels when displayed in center.
/// That means both the icon and text will be shown.
fn width_center(&self) -> i16 {
let icon_width = if let Some(icon) = self.icon {
icon.toif.width() + ICON_RIGHT_PADDING
} else {
0
};
icon_width + self.visible_text_width()
}
/// Painting item on the side if it fits, otherwise paint incomplete if
/// allowed
fn paint_left(&self, area: Rect, show_incomplete: bool) -> Option<i16> {
// When the item does not fit, we stop.
// Rendering the item anyway if the incomplete items are allowed.
if !self.fits(area) {
if show_incomplete {
self.render_left(area);
}
return None;
}
// Rendering the item.
self.render_left(area);
Some(self.width_side())
}
/// Painting item on the side if it fits, otherwise paint incomplete if
/// allowed
fn paint_right(&self, area: Rect, show_incomplete: bool) -> Option<i16> {
// When the item does not fit, we stop.
// Rendering the item anyway if the incomplete items are allowed.
if !self.fits(area) {
if show_incomplete {
self.render_right(area);
}
return None;
}
// Rendering the item.
self.render_right(area);
Some(self.width_side())
}
/// Getting current button layout.
fn btn_layout(&self) -> ButtonLayout<T> {
self.btn_layout.clone()
}
}
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for ChoiceItem<T>
where
T: AsRef<str> + Clone + From<&'static str>,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("ChoiceItem");
t.string("content", &self.text);
}
}

@ -0,0 +1,13 @@
pub mod choice;
pub mod choice_item;
#[cfg(feature = "micropython")]
pub mod number_input;
#[cfg(feature = "micropython")]
pub mod passphrase;
#[cfg(feature = "micropython")]
pub mod pin;
#[cfg(feature = "micropython")]
pub mod simple_choice;
#[cfg(feature = "micropython")]
pub mod wordlist;

@ -0,0 +1,121 @@
use crate::ui::{
component::{Component, Event, EventCtx},
geometry::Rect,
};
use super::super::{ButtonLayout, ChoiceFactory, ChoiceItem, ChoicePage, ChoicePageMsg};
use crate::micropython::buffer::StrBuffer;
use heapless::String;
pub enum NumberInputMsg {
Number(u32),
}
struct ChoiceFactoryNumberInput {
min: u32,
max: u32,
}
impl ChoiceFactoryNumberInput {
fn new(min: u32, max: u32) -> Self {
Self { min, max }
}
}
impl ChoiceFactory<StrBuffer> for ChoiceFactoryNumberInput {
type Item = ChoiceItem<StrBuffer>;
fn count(&self) -> usize {
(self.max - self.min + 1) as usize
}
fn get(&self, choice_index: usize) -> ChoiceItem<StrBuffer> {
let num = self.min + choice_index as u32;
let text: String<10> = String::from(num);
let mut choice_item = ChoiceItem::new(text, ButtonLayout::default_three_icons());
// Disabling prev/next buttons for the first/last choice.
// (could be done to the same button if there is only one)
if choice_index == 0 {
choice_item.set_left_btn(None);
}
if choice_index == self.count() - 1 {
choice_item.set_right_btn(None);
}
choice_item
}
}
/// Simple wrapper around `ChoicePage` that allows for
/// inputting a list of values and receiving the chosen one.
pub struct NumberInput {
choice_page: ChoicePage<ChoiceFactoryNumberInput, StrBuffer>,
min: u32,
}
impl NumberInput {
pub fn new(min: u32, max: u32, init_value: u32) -> Self {
let choices = ChoiceFactoryNumberInput::new(min, max);
let initial_page = init_value - min;
Self {
min,
choice_page: ChoicePage::new(choices).with_initial_page_counter(initial_page as usize),
}
}
}
impl Component for NumberInput {
type Msg = NumberInputMsg;
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_num = self.min + page_counter as u32;
Some(NumberInputMsg::Number(result_num))
}
_ => None,
}
}
fn paint(&mut self) {
self.choice_page.paint();
}
}
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
use super::super::{ButtonAction, ButtonPos};
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for NumberInput {
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();
let current_num = self.min + current_index as u32;
ButtonAction::select_item(inttostr!(current_num))
}
}
}
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("NumberInput");
self.report_btn_actions(t);
t.child("choice_page", &self.choice_page);
}
}

@ -0,0 +1,415 @@
use crate::ui::{
component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx},
display::Icon,
geometry::Rect,
model_tr::{component::ButtonDetails, theme},
util::char_to_string,
};
use super::super::{
ButtonLayout, ChangingTextLine, ChoiceFactory, ChoiceItem, ChoicePage, ChoicePageMsg,
};
use crate::micropython::buffer::StrBuffer;
use heapless::String;
pub enum PassphraseEntryMsg {
Confirmed,
Cancelled,
}
/// Defines the choices currently available on the screen
#[derive(PartialEq, Clone, Copy)]
enum ChoiceCategory {
Menu,
LowercaseLetter,
UppercaseLetter,
Digit,
SpecialSymbol,
}
const MAX_PASSPHRASE_LENGTH: usize = 50;
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; 32] = [
'_', '<', '>', '.', ':', '@', '/', '|', '\\', '!', '(', ')', '+', '%', '&', '-', '[', ']', '?',
'{', '}', ',', '\'', '`', ';', '"', '~', '$', '^', '=', '*', '#',
];
const MENU_LENGTH: usize = 8;
const SHOW_INDEX: usize = 0;
const CANCEL_DELETE_INDEX: usize = 1;
const ENTER_INDEX: usize = 2;
const LOWERCASE_INDEX: usize = 3;
const UPPERCASE_INDEX: usize = 4;
const DIGITS_INDEX: usize = 5;
const SPECIAL_INDEX: usize = 6;
const SPACE_INDEX: usize = 7;
const MENU: [&str; MENU_LENGTH] = [
"SHOW",
"CANCEL_OR_DELETE", // will be chosen dynamically
"ENTER",
"abc",
"ABC",
"123",
"#$!",
"SPACE",
];
/// Get a character at a specified index for a specified category.
fn get_char(current_category: &ChoiceCategory, index: usize) -> char {
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: usize) -> ChoiceCategory {
match page_index {
LOWERCASE_INDEX => ChoiceCategory::LowercaseLetter,
UPPERCASE_INDEX => ChoiceCategory::UppercaseLetter,
DIGITS_INDEX => ChoiceCategory::Digit,
SPECIAL_INDEX => 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) -> usize {
match current_category {
ChoiceCategory::LowercaseLetter => LOWERCASE_LETTERS.len(),
ChoiceCategory::UppercaseLetter => UPPERCASE_LETTERS.len(),
ChoiceCategory::Digit => DIGITS.len(),
ChoiceCategory::SpecialSymbol => SPECIAL_SYMBOLS.len(),
ChoiceCategory::Menu => MENU.len(),
}
}
/// Whether this index is the MENU index - the last one in the list.
fn is_menu_choice(current_category: &ChoiceCategory, page_index: usize) -> 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,
/// Used to either show DELETE or CANCEL
is_empty: bool,
}
impl ChoiceFactoryPassphrase {
fn new(current_category: ChoiceCategory, is_empty: bool) -> Self {
Self {
current_category,
is_empty,
}
}
/// MENU choices with accept and cancel hold-to-confirm side buttons.
fn get_menu_item(&self, choice_index: usize) -> ChoiceItem<StrBuffer> {
// More options for CANCEL/DELETE button
let choice = if choice_index == CANCEL_DELETE_INDEX {
if self.is_empty {
"CANCEL"
} else {
"DELETE"
}
} else {
MENU[choice_index]
};
let mut menu_item = ChoiceItem::new(
String::<50>::from(choice),
ButtonLayout::default_three_icons(),
);
// Action buttons have different middle button text
if [CANCEL_DELETE_INDEX, SHOW_INDEX, ENTER_INDEX].contains(&choice_index) {
let confirm_btn = ButtonDetails::armed_text("CONFIRM".into());
menu_item.set_middle_btn(Some(confirm_btn));
}
// Including icons for some items.
if choice_index == CANCEL_DELETE_INDEX {
if self.is_empty {
menu_item = menu_item.with_icon(Icon::new(theme::ICON_CANCEL));
} else {
menu_item = menu_item.with_icon(Icon::new(theme::ICON_DELETE));
}
} else if choice_index == SHOW_INDEX {
menu_item = menu_item.with_icon(Icon::new(theme::ICON_EYE));
} else if choice_index == ENTER_INDEX {
menu_item = menu_item.with_icon(Icon::new(theme::ICON_TICK));
} else if choice_index == SPACE_INDEX {
menu_item = menu_item.with_icon(Icon::new(theme::ICON_SPACE));
}
menu_item
}
/// Character choices with a BACK to MENU choice at the end (visible from
/// start) to return back
fn get_character_item(&self, choice_index: usize) -> ChoiceItem<StrBuffer> {
if is_menu_choice(&self.current_category, choice_index) {
ChoiceItem::new("BACK", ButtonLayout::arrow_armed_arrow("RETURN".into()))
.with_icon(Icon::new(theme::ICON_ARROW_BACK_UP))
} else {
let ch = get_char(&self.current_category, choice_index);
ChoiceItem::new(char_to_string::<1>(ch), ButtonLayout::default_three_icons())
}
}
}
impl ChoiceFactory<StrBuffer> for ChoiceFactoryPassphrase {
type Item = ChoiceItem<StrBuffer>;
fn count(&self) -> usize {
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,
}
}
fn get(&self, choice_index: usize) -> ChoiceItem<StrBuffer> {
match self.current_category {
ChoiceCategory::Menu => self.get_menu_item(choice_index),
_ => self.get_character_item(choice_index),
}
}
}
/// Component for entering a passphrase.
pub struct PassphraseEntry {
choice_page: ChoicePage<ChoiceFactoryPassphrase, StrBuffer>,
passphrase_dots: Child<ChangingTextLine<String<MAX_PASSPHRASE_LENGTH>>>,
show_plain_passphrase: bool,
textbox: TextBox<MAX_PASSPHRASE_LENGTH>,
current_category: ChoiceCategory,
menu_position: usize, // position in the menu so we can return back
}
impl PassphraseEntry {
pub fn new() -> Self {
Self {
choice_page: ChoicePage::new(ChoiceFactoryPassphrase::new(ChoiceCategory::Menu, true))
.with_carousel(true)
.with_initial_page_counter(LOWERCASE_INDEX),
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) {
let text_to_show = if self.show_plain_passphrase {
String::from(self.passphrase())
} else {
let mut dots: String<MAX_PASSPHRASE_LENGTH> = String::new();
for _ in 0..self.textbox.len() {
unwrap!(dots.push_str("*"));
}
dots
};
self.passphrase_dots.mutate(ctx, |ctx, passphrase_dots| {
passphrase_dots.update_text(text_to_show);
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.is_empty());
// Going back to the last MENU position before showing the MENU
self.choice_page
.reset(ctx, menu_choices, Some(self.menu_position), true);
}
/// Displaying the character category
fn show_category_page(&mut self, ctx: &mut EventCtx) {
let category_choices = ChoiceFactoryPassphrase::new(self.current_category, self.is_empty());
self.choice_page.reset(ctx, category_choices, Some(0), true);
}
pub fn passphrase(&self) -> &str {
self.textbox.content()
}
fn is_empty(&self) -> bool {
self.textbox.is_empty()
}
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);
}
if let Some(ChoicePageMsg::Choice(page_counter)) = self.choice_page.event(ctx, event) {
// Event handling based on MENU vs CATEGORY
if self.current_category == ChoiceCategory::Menu {
// Going to new category, applying some action or returning the result
match page_counter {
CANCEL_DELETE_INDEX => {
if self.is_empty() {
return Some(PassphraseEntryMsg::Cancelled);
} else {
self.delete_last_digit(ctx);
self.update_passphrase_dots(ctx);
if self.is_empty() {
// Allowing for DELETE/CANCEL change
self.menu_position = CANCEL_DELETE_INDEX;
self.show_menu_page(ctx);
}
ctx.request_paint();
}
}
ENTER_INDEX => {
return Some(PassphraseEntryMsg::Confirmed);
}
SHOW_INDEX => {
self.show_plain_passphrase = true;
self.update_passphrase_dots(ctx);
ctx.request_paint();
}
SPACE_INDEX => {
if !self.is_full() {
self.append_char(ctx, ' ');
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();
}
}
} else {
// Coming back to MENU or adding new character
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();
}
}
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
use super::super::{ButtonAction, ButtonPos};
#[cfg(feature = "ui_debug")]
use crate::ui::util;
#[cfg(feature = "ui_debug")]
impl ChoiceCategory {
fn string(&self) -> String<25> {
match self {
ChoiceCategory::Menu => "MENU".into(),
ChoiceCategory::LowercaseLetter => MENU[LOWERCASE_INDEX].into(),
ChoiceCategory::UppercaseLetter => MENU[UPPERCASE_INDEX].into(),
ChoiceCategory::Digit => MENU[DIGITS_INDEX].into(),
ChoiceCategory::SpecialSymbol => MENU[SPECIAL_INDEX].into(),
}
}
}
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for PassphraseEntry {
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();
match &self.current_category {
ChoiceCategory::Menu => ButtonAction::select_item(MENU[current_index]),
_ => {
// There is "MENU" option at the end
match self.choice_page.has_next_choice() {
false => ButtonAction::Action("BACK").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.component("PassphraseKeyboard");
t.string("passphrase", self.textbox.content());
t.string("current_category", &self.current_category.string());
self.report_btn_actions(t);
t.child("choice_page", &self.choice_page);
}
}

@ -0,0 +1,257 @@
use crate::{
micropython::buffer::StrBuffer,
trezorhal::random,
ui::{
component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx},
display::Icon,
geometry::Rect,
model_tr::theme,
},
};
use super::super::{
ButtonDetails, ButtonLayout, ChangingTextLine, ChoiceFactory, ChoiceItem, ChoicePage,
ChoicePageMsg,
};
use heapless::String;
pub enum PinEntryMsg {
Confirmed,
Cancelled,
}
const MAX_PIN_LENGTH: usize = 50;
const CHOICE_LENGTH: usize = 13;
const DELETE_INDEX: usize = 0;
const SHOW_INDEX: usize = 1;
const ENTER_INDEX: usize = 2;
const NUMBER_START_INDEX: usize = 3;
const CHOICES: [&str; CHOICE_LENGTH] = [
"DELETE", "SHOW", "ENTER", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
];
struct ChoiceFactoryPIN {}
impl ChoiceFactoryPIN {
fn new() -> Self {
Self {}
}
}
impl ChoiceFactory<StrBuffer> for ChoiceFactoryPIN {
type Item = ChoiceItem<StrBuffer>;
fn get(&self, choice_index: usize) -> ChoiceItem<StrBuffer> {
let choice_str = CHOICES[choice_index];
let mut choice_item = ChoiceItem::new(choice_str, ButtonLayout::default_three_icons());
// Action buttons have different middle button text
if [DELETE_INDEX, SHOW_INDEX, ENTER_INDEX].contains(&(choice_index)) {
let confirm_btn = ButtonDetails::armed_text("CONFIRM".into());
choice_item.set_middle_btn(Some(confirm_btn));
}
// Adding icons for appropriate items
if choice_index == DELETE_INDEX {
choice_item = choice_item.with_icon(Icon::new(theme::ICON_DELETE));
} else if choice_index == SHOW_INDEX {
choice_item = choice_item.with_icon(Icon::new(theme::ICON_EYE));
} else if choice_index == ENTER_INDEX {
choice_item = choice_item.with_icon(Icon::new(theme::ICON_TICK));
}
choice_item
}
fn count(&self) -> usize {
CHOICE_LENGTH
}
}
/// Component for entering a PIN.
pub struct PinEntry {
choice_page: ChoicePage<ChoiceFactoryPIN, StrBuffer>,
pin_line: Child<ChangingTextLine<String<MAX_PIN_LENGTH>>>,
subprompt_line: Child<ChangingTextLine<StrBuffer>>,
prompt: StrBuffer,
show_real_pin: bool,
textbox: TextBox<MAX_PIN_LENGTH>,
}
impl PinEntry {
pub fn new(prompt: StrBuffer, subprompt: StrBuffer) -> Self {
let choices = ChoiceFactoryPIN::new();
Self {
// Starting at the digit 0
choice_page: ChoicePage::new(choices)
.with_initial_page_counter(NUMBER_START_INDEX)
.with_carousel(true),
pin_line: Child::new(ChangingTextLine::center_bold(String::from(
prompt.clone().as_ref(),
))),
subprompt_line: Child::new(ChangingTextLine::center_mono(subprompt)),
prompt,
show_real_pin: false,
textbox: TextBox::empty(),
}
}
fn append_new_digit(&mut self, ctx: &mut EventCtx, page_counter: usize) {
let digit = CHOICES[page_counter];
self.textbox.append_slice(ctx, digit);
}
fn delete_last_digit(&mut self, ctx: &mut EventCtx) {
self.textbox.delete_last(ctx);
}
/// Performs overall update of the screen.
fn update(&mut self, ctx: &mut EventCtx) {
self.update_header_info(ctx);
ctx.request_paint();
}
/// Update the header information - (sub)prompt and visible PIN.
/// If PIN is empty, showing prompt in `pin_line` and sub-prompt in the
/// `subprompt_line`. Otherwise disabling the `subprompt_line` and showing
/// the PIN - either in real numbers or masked in asterisks.
fn update_header_info(&mut self, ctx: &mut EventCtx) {
let show_prompts = self.is_empty();
let text = if show_prompts {
String::from(self.prompt.as_ref())
} else if self.show_real_pin {
String::from(self.pin())
} else {
let mut dots: String<MAX_PIN_LENGTH> = String::new();
for _ in 0..self.textbox.len() {
unwrap!(dots.push_str("*"));
}
dots
};
// Force repaint of the whole header.
// Putting the current text into the PIN line.
self.pin_line.mutate(ctx, |ctx, pin_line| {
pin_line.update_text(text);
pin_line.request_complete_repaint(ctx);
});
// Showing subprompt only conditionally.
self.subprompt_line.mutate(ctx, |ctx, subprompt_line| {
subprompt_line.show_or_not(show_prompts);
subprompt_line.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_height = self.pin_line.inner().needed_height();
let subtitle_height = self.subprompt_line.inner().needed_height();
let (title_area, subtitle_and_choice_area) = bounds.split_top(pin_height);
let (subtitle_area, choice_area) = subtitle_and_choice_area.split_top(subtitle_height);
self.pin_line.place(title_area);
self.subprompt_line.place(subtitle_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(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 {
DELETE_INDEX => {
self.delete_last_digit(ctx);
self.update(ctx);
}
SHOW_INDEX => {
self.show_real_pin = true;
self.update(ctx);
}
ENTER_INDEX => return Some(PinEntryMsg::Confirmed),
_ => {
if !self.is_full() {
self.append_new_digit(ctx, page_counter);
// Choosing random digit to be shown next, but different
// from the current choice.
let new_page_counter = random::uniform_between_except(
NUMBER_START_INDEX as u32,
(CHOICE_LENGTH - 1) as u32,
page_counter as u32,
);
self.choice_page
.set_page_counter(ctx, new_page_counter as usize);
self.update(ctx);
}
}
}
}
None
}
fn paint(&mut self) {
self.pin_line.paint();
self.subprompt_line.paint();
self.choice_page.paint();
}
}
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
use super::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();
match current_index {
DELETE_INDEX => ButtonAction::Action("DELETE").string(),
SHOW_INDEX => ButtonAction::Action("SHOW").string(),
ENTER_INDEX => ButtonAction::Action("ENTER").string(),
_ => ButtonAction::select_item(CHOICES[current_index]),
}
}
}
}
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("PinKeyboard");
t.string("prompt", self.prompt.as_ref());
let subprompt = self.subprompt_line.inner().get_text();
if !subprompt.is_empty() {
t.string("subprompt", subprompt);
}
t.string("pin", self.textbox.content());
self.report_btn_actions(t);
t.child("choice_page", &self.choice_page);
}
}

@ -0,0 +1,147 @@
use crate::{
micropython::buffer::StrBuffer,
ui::{
component::{Component, Event, EventCtx},
geometry::Rect,
},
};
use super::super::{ButtonLayout, ChoiceFactory, ChoiceItem, ChoicePage, ChoicePageMsg};
use heapless::{String, Vec};
pub enum SimpleChoiceMsg {
Result(String<50>),
Index(usize),
}
struct ChoiceFactorySimple<const N: usize> {
choices: Vec<StrBuffer, N>,
carousel: bool,
}
impl<const N: usize> ChoiceFactorySimple<N> {
fn new(choices: Vec<StrBuffer, N>, carousel: bool) -> Self {
Self { choices, carousel }
}
}
impl<const N: usize> ChoiceFactory<StrBuffer> for ChoiceFactorySimple<N> {
type Item = ChoiceItem<StrBuffer>;
fn count(&self) -> usize {
N
}
fn get(&self, choice_index: usize) -> ChoiceItem<StrBuffer> {
let text = &self.choices[choice_index];
let mut choice_item = ChoiceItem::new(text, ButtonLayout::default_three_icons());
// Disabling prev/next buttons for the first/last choice when not in carousel.
// (could be done to the same button if there is only one)
if !self.carousel {
if choice_index == 0 {
choice_item.set_left_btn(None);
}
if choice_index == N - 1 {
choice_item.set_right_btn(None);
}
}
choice_item
}
}
/// Simple wrapper around `ChoicePage` that allows for
/// inputting a list of values and receiving the chosen one.
pub struct SimpleChoice<const N: usize> {
choices: Vec<StrBuffer, N>,
choice_page: ChoicePage<ChoiceFactorySimple<N>, StrBuffer>,
return_index: bool,
}
impl<const N: usize> SimpleChoice<N> {
pub fn new(str_choices: Vec<StrBuffer, N>, carousel: bool) -> Self {
let choices = ChoiceFactorySimple::new(str_choices.clone(), carousel);
Self {
choices: str_choices,
choice_page: ChoicePage::new(choices).with_carousel(carousel),
return_index: false,
}
}
/// Show only the currently selected item, nothing left/right.
pub fn with_only_one_item(mut self) -> Self {
self.choice_page = self.choice_page.with_only_one_item(true);
self
}
/// Show choices even when they do not fit entirely.
pub fn with_show_incomplete(mut self) -> Self {
self.choice_page = self.choice_page.with_incomplete(true);
self
}
/// Returning chosen page index instead of the string result.
pub fn with_return_index(mut self) -> Self {
self.return_index = true;
self
}
}
impl<const N: usize> Component for SimpleChoice<N> {
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)) => {
if self.return_index {
Some(SimpleChoiceMsg::Index(page_counter))
} else {
let result = String::from(self.choices[page_counter].as_ref());
Some(SimpleChoiceMsg::Result(result))
}
}
_ => None,
}
}
fn paint(&mut self) {
self.choice_page.paint();
}
}
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
use super::super::{ButtonAction, ButtonPos};
#[cfg(feature = "ui_debug")]
impl<const N: usize> crate::trace::Trace for SimpleChoice<N> {
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();
ButtonAction::select_item(self.choices[current_index].as_ref())
}
}
}
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("SimpleChoice");
self.report_btn_actions(t);
t.child("choice_page", &self.choice_page);
}
}

@ -0,0 +1,301 @@
use crate::{
trezorhal::wordlist::Wordlist,
ui::{
component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx},
display::Icon,
geometry::Rect,
model_tr::theme,
util::char_to_string,
},
};
use super::super::{
ButtonLayout, ChangingTextLine, ChoiceFactory, ChoiceItem, ChoicePage, ChoicePageMsg,
};
use crate::micropython::buffer::StrBuffer;
use heapless::{String, Vec};
pub enum WordlistEntryMsg {
ResultWord(String<15>),
}
const MAX_WORD_LENGTH: usize = 10;
const MAX_LETTERS_LENGTH: usize = 26;
/// Offer words when there will be fewer of them than this
const OFFER_WORDS_THRESHOLD: usize = 10;
/// Where will be the DELETE option - at the first position
const DELETE_INDEX: usize = 0;
/// Which index will be used at the beginning.
/// (Accounts for DELETE to be at index 0)
const INITIAL_PAGE_COUNTER: usize = DELETE_INDEX + 1;
const PROMPT: &str = "_";
/// Type of the wordlist, deciding the list of words to be used
pub enum WordlistType {
Bip39,
Slip39,
}
/// We are offering either letters or words.
enum ChoiceFactoryWordlist {
Letters(Vec<char, MAX_LETTERS_LENGTH>),
Words(Vec<&'static str, OFFER_WORDS_THRESHOLD>),
}
impl ChoiceFactoryWordlist {
fn letters(letter_choices: Vec<char, MAX_LETTERS_LENGTH>) -> Self {
Self::Letters(letter_choices)
}
fn words(word_choices: Vec<&'static str, OFFER_WORDS_THRESHOLD>) -> Self {
Self::Words(word_choices)
}
}
impl ChoiceFactory<StrBuffer> for ChoiceFactoryWordlist {
type Item = ChoiceItem<StrBuffer>;
fn count(&self) -> usize {
// Accounting for the DELETE option
match self {
Self::Letters(letter_choices) => letter_choices.len() + 1,
Self::Words(word_choices) => word_choices.len() + 1,
}
}
fn get(&self, choice_index: usize) -> ChoiceItem<StrBuffer> {
// Letters have a carousel, words do not
// Putting DELETE as the first option in both cases
// (is a requirement for WORDS, doing it for LETTERS as well to unite it)
match self {
Self::Letters(letter_choices) => {
if choice_index == DELETE_INDEX {
ChoiceItem::new("DELETE", ButtonLayout::arrow_armed_arrow("CONFIRM".into()))
.with_icon(Icon::new(theme::ICON_DELETE))
} else {
let letter = letter_choices[choice_index - 1];
ChoiceItem::new(
char_to_string::<1>(letter),
ButtonLayout::default_three_icons(),
)
}
}
Self::Words(word_choices) => {
if choice_index == DELETE_INDEX {
ChoiceItem::new("DELETE", ButtonLayout::none_armed_arrow("CONFIRM".into()))
.with_icon(Icon::new(theme::ICON_DELETE))
} else {
let word = word_choices[choice_index - 1];
let mut item = ChoiceItem::new(word, ButtonLayout::default_three_icons());
if choice_index == self.count() - 1 {
item.set_right_btn(None);
}
item
}
}
}
}
}
/// Component for entering a mnemonic from a wordlist - BIP39 or SLIP39.
pub struct WordlistEntry {
choice_page: ChoicePage<ChoiceFactoryWordlist, StrBuffer>,
chosen_letters: Child<ChangingTextLine<String<{ MAX_WORD_LENGTH + 1 }>>>,
letter_choices: Vec<char, MAX_LETTERS_LENGTH>,
textbox: TextBox<MAX_WORD_LENGTH>,
offer_words: bool,
words_list: Wordlist,
wordlist_type: WordlistType,
}
impl WordlistEntry {
pub fn new(wordlist_type: WordlistType) -> Self {
let words_list = Self::get_fresh_wordlist(&wordlist_type);
let letter_choices: Vec<char, MAX_LETTERS_LENGTH> =
words_list.get_available_letters("").collect();
let choices = ChoiceFactoryWordlist::letters(letter_choices.clone());
Self {
// Starting at second page because of DELETE option
choice_page: ChoicePage::new(choices)
.with_incomplete(true)
.with_carousel(true)
.with_initial_page_counter(INITIAL_PAGE_COUNTER),
chosen_letters: Child::new(ChangingTextLine::center_mono(String::from(PROMPT))),
letter_choices,
textbox: TextBox::empty(),
offer_words: false,
words_list,
wordlist_type,
}
}
/// Get appropriate wordlist with all possible words
fn get_fresh_wordlist(wordlist_type: &WordlistType) -> Wordlist {
match wordlist_type {
WordlistType::Bip39 => Wordlist::bip39(),
WordlistType::Slip39 => Wordlist::slip39(),
}
}
/// Gets up-to-date choices for letters or words.
fn get_current_choices(&mut self) -> ChoiceFactoryWordlist {
// 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();
ChoiceFactoryWordlist::words(word_choices)
} else {
self.offer_words = false;
self.letter_choices = self
.words_list
.get_available_letters(self.textbox.content())
.collect();
ChoiceFactoryWordlist::letters(self.letter_choices.clone())
}
}
/// Updates the whole page.
fn update(&mut self, ctx: &mut EventCtx) {
self.update_chosen_letters(ctx);
let new_choices = self.get_current_choices();
// Not using carousel in case of words, as that looks weird in case
// there is only one word to choose from.
self.choice_page.reset(
ctx,
new_choices,
Some(INITIAL_PAGE_COUNTER),
!self.offer_words,
);
ctx.request_paint();
}
/// Reflects currently chosen letters in the textbox.
fn update_chosen_letters(&mut self, ctx: &mut EventCtx) {
let text = build_string!({ MAX_WORD_LENGTH + 1 }, self.textbox.content(), PROMPT);
self.chosen_letters.mutate(ctx, |ctx, chosen_letters| {
chosen_letters.update_text(text);
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 = Self::get_fresh_wordlist(&self.wordlist_type);
}
}
impl Component for WordlistEntry {
type Msg = WordlistEntryMsg;
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);
if let Some(ChoicePageMsg::Choice(page_counter)) = msg {
// Clicked SELECT.
// When we already offer words, return the word at the given index.
// Otherwise, resetting the choice page with up-to-date choices.
if page_counter == DELETE_INDEX {
// Clicked DELETE. Deleting last letter, updating wordlist and updating choices
self.delete_last_letter(ctx);
self.reset_wordlist();
self.update(ctx);
} else {
let index = page_counter - 1;
if self.offer_words {
let word = self.words_list.get(index).unwrap_or_default();
return Some(WordlistEntryMsg::ResultWord(String::from(word)));
} else {
let new_letter = self.letter_choices[index];
self.append_letter(ctx, new_letter);
self.update(ctx);
}
}
}
None
}
fn paint(&mut self) {
self.chosen_letters.paint();
self.choice_page.paint();
}
}
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
use super::super::{ButtonAction, ButtonPos};
#[cfg(feature = "ui_debug")]
use crate::ui::util;
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for WordlistEntry {
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();
let choice: String<10> = if current_index == DELETE_INDEX {
String::from("DELETE")
} else {
let index = current_index - 1;
if self.offer_words {
self.words_list.get(index).unwrap_or_default().into()
} else {
util::char_to_string(self.letter_choices[index])
}
};
ButtonAction::select_item(choice)
}
}
}
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
match self.wordlist_type {
WordlistType::Bip39 => t.component("Bip39Entry"),
WordlistType::Slip39 => t.component("Slip39Entry"),
}
t.string("textbox", self.textbox.content());
self.report_btn_actions(t);
if self.offer_words {
t.in_list("word_choices", &|list_t| {
for word in self.words_list.iter() {
list_t.string(word);
}
});
} else {
t.in_list("letter_choices", &|list_t| {
for ch in &self.letter_choices {
list_t.string(&util::char_to_string::<1>(*ch));
}
});
}
t.child("choice_page", &self.choice_page);
}
}

@ -5,6 +5,8 @@ use crate::{
component::{Component, Event, EventCtx},
display::{self, Color, Font},
geometry::{Offset, Rect},
model_tr::theme,
util::animation_disabled,
},
};
@ -20,31 +22,69 @@ enum State {
Grown,
}
pub struct Loader {
pub struct Loader<T>
where
T: AsRef<str> + Clone + From<&'static str>,
{
area: Rect,
state: State,
growing_duration: Duration,
shrinking_duration: Duration,
text: display::TextOverlay<'static>,
text_overlay: display::TextOverlay<T>,
styles: LoaderStyleSheet,
}
impl Loader {
impl<T> Loader<T>
where
T: AsRef<str> + Clone + From<&'static str>,
{
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: 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(text_overlay, 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.get_text()
}
/// Change the text of the loader.
pub fn set_text(&mut self, text: T) {
self.text_overlay.set_text(text);
}
/// 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 +151,32 @@ 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) {
// 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),
Some(&self.text_overlay),
style.fg_color,
style.bg_color,
-1,
invert_from,
invert_from as i16,
);
}
}
impl Component for Loader {
impl<T> Component for Loader<T>
where
T: AsRef<str> + Clone + From<&'static str>,
{
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);
// Centering the text in the loader rectangle.
let baseline = bounds.bottom_center() + Offset::new(1, -2);
self.text_overlay.place(baseline);
self.area
}
@ -140,18 +185,21 @@ 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.
if !animation_disabled() {
ctx.request_paint();
}
}
}
}
@ -169,11 +217,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 +239,25 @@ pub struct LoaderStyle {
pub bg_color: Color,
}
impl LoaderStyleSheet {
pub fn default_loader() -> Self {
Self {
normal: &LoaderStyle {
font: theme::FONT_BUTTON,
fg_color: theme::FG,
bg_color: theme::BG,
},
}
}
}
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for Loader {
impl<T> crate::trace::Trace for Loader<T>
where
T: AsRef<str> + Clone + From<&'static str>,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("Loader");
}

@ -1,21 +1,98 @@
mod button;
mod confirm;
mod dialog;
mod frame;
mod button_controller;
mod common;
mod hold_to_confirm;
mod input_methods;
mod loader;
mod page;
mod result;
mod welcome_screen;
use super::theme;
pub use button::{
Button, ButtonAction, ButtonActions, ButtonContent, ButtonDetails, ButtonLayout, ButtonMsg,
ButtonPos, ButtonStyle, ButtonStyleSheet,
};
pub use button_controller::{ButtonController, ButtonControllerMsg};
pub use hold_to_confirm::{HoldToConfirm, HoldToConfirmMsg};
pub use input_methods::{
choice::{Choice, ChoiceFactory, ChoicePage, ChoicePageMsg},
choice_item::ChoiceItem,
};
pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet};
pub use result::ResultScreen;
pub use welcome_screen::WelcomeScreen;
#[cfg(feature = "micropython")]
mod address_details;
#[cfg(feature = "micropython")]
mod changing_text;
#[cfg(feature = "micropython")]
mod coinjoin_progress;
#[cfg(feature = "micropython")]
mod flow;
#[cfg(feature = "micropython")]
mod flow_pages;
#[cfg(feature = "micropython")]
mod flow_pages_helpers;
#[cfg(feature = "micropython")]
mod frame;
#[cfg(feature = "micropython")]
mod homescreen;
#[cfg(feature = "micropython")]
mod no_btn_dialog;
#[cfg(feature = "micropython")]
mod page;
#[cfg(feature = "micropython")]
mod progress;
#[cfg(feature = "micropython")]
mod result_anim;
#[cfg(feature = "micropython")]
mod result_popup;
#[cfg(feature = "micropython")]
mod scrollbar;
#[cfg(feature = "micropython")]
mod share_words;
#[cfg(feature = "micropython")]
mod show_more;
#[cfg(feature = "micropython")]
mod title;
use super::theme;
#[cfg(feature = "micropython")]
pub use address_details::{AddressDetails, AddressDetailsMsg};
pub use button::{Button, ButtonContent, ButtonMsg, ButtonPos, ButtonStyle, ButtonStyleSheet};
pub use confirm::{HoldToConfirm, HoldToConfirmMsg};
pub use dialog::{Dialog, DialogMsg};
pub use frame::Frame;
pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet};
#[cfg(feature = "micropython")]
pub use changing_text::ChangingTextLine;
#[cfg(feature = "micropython")]
pub use coinjoin_progress::CoinJoinProgress;
#[cfg(feature = "micropython")]
pub use flow::{Flow, FlowMsg};
#[cfg(feature = "micropython")]
pub use flow_pages::{FlowPages, Page};
#[cfg(feature = "micropython")]
pub use frame::{Frame, ScrollableContent, ScrollableFrame};
#[cfg(feature = "micropython")]
pub use homescreen::{Homescreen, HomescreenMsg, Lockscreen};
#[cfg(feature = "micropython")]
pub use input_methods::{
number_input::{NumberInput, NumberInputMsg},
passphrase::{PassphraseEntry, PassphraseEntryMsg},
pin::{PinEntry, PinEntryMsg},
simple_choice::{SimpleChoice, SimpleChoiceMsg},
wordlist::{WordlistEntry, WordlistEntryMsg, WordlistType},
};
#[cfg(feature = "micropython")]
pub use no_btn_dialog::{NoBtnDialog, NoBtnDialogMsg};
#[cfg(feature = "micropython")]
pub use page::ButtonPage;
pub use result::ResultScreen;
#[cfg(feature = "micropython")]
pub use progress::Progress;
#[cfg(feature = "micropython")]
pub use result_anim::{ResultAnim, ResultAnimMsg};
#[cfg(feature = "micropython")]
pub use result_popup::{ResultPopup, ResultPopupMsg};
#[cfg(feature = "micropython")]
pub use scrollbar::ScrollBar;
#[cfg(feature = "micropython")]
pub use share_words::ShareWords;
#[cfg(feature = "micropython")]
pub use show_more::{CancelInfoConfirmMsg, ShowMore};

@ -0,0 +1,70 @@
use crate::ui::{
component::{Child, Component, Event, EventCtx},
geometry::Rect,
};
pub enum NoBtnDialogMsg<T> {
Controls(T),
}
/// Used for simple displaying of information without user interaction.
/// Suitable for just showing a message, or having a timeout after which
/// the dialog is dismissed.
pub struct NoBtnDialog<T, U> {
content: Child<T>,
controls: Child<U>,
}
impl<T, U> NoBtnDialog<T, U>
where
T: Component,
U: Component,
{
pub fn new(content: T, controls: U) -> Self {
Self {
content: Child::new(content),
controls: Child::new(controls),
}
}
pub fn inner(&self) -> &T {
self.content.inner()
}
}
impl<T, U> Component for NoBtnDialog<T, U>
where
T: Component,
U: Component,
{
type Msg = NoBtnDialogMsg<U::Msg>;
fn place(&mut self, bounds: Rect) -> Rect {
self.controls.place(bounds);
self.content.place(bounds);
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
self.controls.event(ctx, event).map(Self::Msg::Controls)
}
fn paint(&mut self) {
self.content.paint();
self.controls.paint();
}
}
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
impl<T, U> crate::trace::Trace for NoBtnDialog<T, U>
where
T: crate::trace::Trace,
U: crate::trace::Trace,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("NoBtnDialog");
t.child("content", &self.content);
}
}

@ -1,153 +1,70 @@
use crate::ui::{
component::{Component, ComponentExt, Event, EventCtx, Never, Pad, PageMsg, Paginate},
display::{self, Color, Font},
geometry::{Insets, Offset, Point, Rect},
component::{Child, Component, ComponentExt, Event, EventCtx, Pad, PageMsg, Paginate},
display::Color,
geometry::{Insets, Rect},
model_tr::constant,
};
use super::{theme, Button, ButtonMsg, ButtonPos};
use super::{theme, ButtonController, ButtonControllerMsg, ButtonDetails, ButtonLayout, ButtonPos};
pub struct ButtonPage<T> {
content: T,
scrollbar: ScrollBar,
page_count: usize,
active_page: usize,
content: Child<T>,
pad: Pad,
prev: Button<&'static str>,
next: Button<&'static str>,
cancel: Button<&'static str>,
confirm: Button<&'static str>,
/// Left button of the first screen
cancel_btn_details: Option<ButtonDetails<StrBuffer>>,
/// Right button of the last screen
confirm_btn_details: Option<ButtonDetails<StrBuffer>>,
/// Left button of the last page
last_back_btn_details: Option<ButtonDetails<StrBuffer>>,
/// Left button of every screen in the middle
back_btn_details: Option<ButtonDetails<StrBuffer>>,
/// Right button of every screen apart the last one
next_btn_details: Option<ButtonDetails<StrBuffer>>,
buttons: Child<ButtonController<StrBuffer>>,
}
impl<T> ButtonPage<T>
where
T: Paginate,
T: Component,
T: Component + Paginate,
{
pub fn new(content: T, background: Color) -> Self {
Self {
content,
scrollbar: ScrollBar::vertical(),
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()),
page_count: 0, // will be set in place()
active_page: 0,
content: Child::new(content),
pad: Pad::with_background(background).with_clear(),
cancel_btn_details: Some(ButtonDetails::cancel_icon()),
confirm_btn_details: Some(ButtonDetails::text("CONFIRM".into())),
back_btn_details: Some(ButtonDetails::up_arrow_icon_wide()),
last_back_btn_details: Some(ButtonDetails::up_arrow_icon()),
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();
pub fn with_cancel_btn(mut self, btn_details: Option<ButtonDetails<StrBuffer>>) -> Self {
self.cancel_btn_details = btn_details;
self
}
}
impl<T> Component for ButtonPage<T>
where
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_area = content_area.inset(Insets::top(1));
self.pad.place(bounds);
self.content.place(content_area);
let page_count = self.content.page_count();
self.scrollbar.set_count_and_active_page(page_count, 0);
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);
bounds
pub fn with_confirm_btn(mut self, btn_details: Option<ButtonDetails<StrBuffer>>) -> Self {
self.confirm_btn_details = btn_details;
self
}
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));
}
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;
}
} 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) {
return Some(PageMsg::Content(msg));
}
None
pub fn with_back_btn(mut self, btn_details: Option<ButtonDetails<StrBuffer>>) -> Self {
self.back_btn_details = btn_details;
self
}
fn paint(&mut self) {
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();
}
}
}
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for ButtonPage<T>
where
T: crate::trace::Trace,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("ButtonPage");
t.int("active_page", self.scrollbar.active_page as i64);
t.int("page_count", self.scrollbar.page_count as i64);
t.child("content", &self.content);
}
}
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 with_next_btn(mut self, btn_details: Option<ButtonDetails<StrBuffer>>) -> Self {
self.next_btn_details = btn_details;
self
}
pub fn has_next_page(&self) -> bool {
@ -166,61 +83,182 @@ impl ScrollBar {
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)
/// 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) {
self.content.mutate(ctx, |ctx, content| {
content.change_page(self.active_page);
content.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.has_previous_page(), self.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<StrBuffer> {
let btn_left = self.get_left_button_details(!has_prev, !has_next);
let btn_right = self.get_right_button_details(has_next);
ButtonLayout::new(btn_left, None, btn_right)
}
/// Get the left button details, depending whether the page is first, last,
/// or in the middle.
fn get_left_button_details(
&self,
is_first: bool,
is_last: bool,
) -> Option<ButtonDetails<StrBuffer>> {
if is_first {
self.cancel_btn_details.clone()
} else if is_last {
self.last_back_btn_details.clone()
} else {
self.back_btn_details.clone()
}
if active {
display::rect_fill(
Rect::from_top_left_and_size(top_left, Self::DOT_SIZE).inset(Insets::uniform(1)),
theme::FG,
)
}
/// Get the right button details, depending on whether there is a next
/// page.
fn get_right_button_details(&self, has_next_page: bool) -> Option<ButtonDetails<StrBuffer>> {
if has_next_page {
self.next_btn_details.clone()
} else {
self.confirm_btn_details.clone()
}
}
}
impl Component for ScrollBar {
type Msg = Never;
impl<T> ScrollableContent for ButtonPage<T> {
fn page_count(&self) -> usize {
self.page_count
}
fn active_page(&self) -> usize {
self.active_page
}
}
impl<T> Component for ButtonPage<T>
where
T: Component + Paginate,
{
type Msg = PageMsg<T::Msg, bool>;
fn place(&mut self, bounds: Rect) -> Rect {
self.area = bounds;
self.area
let (content_area, button_area) = bounds.split_bottom(theme::BUTTON_HEIGHT);
// Pad only the content, buttons handle it themselves.
self.pad.place(content_area);
// Moving the content LINE_SPACE pixels down, otherwise the top would not be
// padded correctly
self.content
.place(content_area.inset(Insets::top(constant::LINE_SPACE)));
// Need to be called here, only after content is placed
// and we can calculate the page count.
self.page_count = self.content.inner_mut().page_count();
self.set_buttons_for_initial_page(self.page_count);
self.buttons.place(button_area);
bounds
}
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
ctx.set_page_count(self.page_count());
if let Some(ButtonControllerMsg::Triggered(pos)) = self.buttons.event(ctx, event) {
match pos {
ButtonPos::Left => {
if self.has_previous_page() {
// Clicked BACK. Scroll up.
self.go_to_previous_page();
self.change_page(ctx);
} else {
// Clicked CANCEL. Send result.
return Some(PageMsg::Controls(false));
}
}
ButtonPos::Right => {
if self.has_next_page() {
// Clicked NEXT. Scroll down.
self.go_to_next_page();
self.change_page(ctx);
} else {
// Clicked CONFIRM. Send result.
return Some(PageMsg::Controls(true));
}
}
_ => {}
}
}
if let Some(msg) = self.content.event(ctx, event) {
return Some(PageMsg::Content(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
self.pad.paint();
self.content.paint();
self.buttons.paint();
}
}
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
use super::ButtonAction;
use crate::{micropython::buffer::StrBuffer, ui::model_tr::component::frame::ScrollableContent};
#[cfg(feature = "ui_debug")]
use heapless::String;
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for ButtonPage<T>
where
T: crate::trace::Trace + Paginate + Component,
{
fn get_btn_action(&self, pos: ButtonPos) -> String<25> {
match pos {
ButtonPos::Left => {
if self.has_previous_page() {
ButtonAction::PrevPage.string()
} else if self.cancel_btn_details.is_some() {
ButtonAction::Cancel.string()
} else {
ButtonAction::empty()
}
}
};
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
ButtonPos::Right => {
if self.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.component("ButtonPage");
t.int("active_page", self.active_page as i64);
t.int("page_count", self.page_count as i64);
self.report_btn_actions(t);
t.child("buttons", &self.buttons);
t.child("content", &self.content);
}
}

@ -0,0 +1,131 @@
use core::mem;
use crate::{
error::Error,
micropython::buffer::StrBuffer,
ui::{
component::{
base::ComponentExt,
paginated::Paginate,
text::paragraphs::{Paragraph, Paragraphs},
Child, Component, Event, EventCtx, Label, Never, Pad,
},
display::{self, Font},
geometry::Rect,
model_tr::constant,
util::animation_disabled,
},
};
use super::theme;
pub struct Progress {
title: Child<Label<StrBuffer>>,
value: u16,
loader_y_offset: i16,
indeterminate: bool,
description: Child<Paragraphs<Paragraph<StrBuffer>>>,
description_pad: Pad,
update_description: fn(&str) -> Result<StrBuffer, Error>,
}
impl Progress {
const AREA: Rect = constant::screen();
pub fn new(
title: StrBuffer,
indeterminate: bool,
description: StrBuffer,
update_description: fn(&str) -> Result<StrBuffer, Error>,
) -> Self {
Self {
title: Label::centered(title, theme::TEXT_BOLD).into_child(),
value: 0,
loader_y_offset: 0,
indeterminate,
description: Paragraphs::new(
Paragraph::new(&theme::TEXT_NORMAL, description).centered(),
)
.into_child(),
description_pad: Pad::with_background(theme::BG),
update_description,
}
}
}
impl Component for Progress {
type Msg = Never;
fn place(&mut self, _bounds: Rect) -> Rect {
let description_lines = 1 + self
.description
.inner()
.inner()
.content()
.as_ref()
.chars()
.filter(|c| *c == '\n')
.count() as i16;
let (title, rest) = Self::AREA.split_top(self.title.inner().max_size().y);
let (loader, description) =
rest.split_bottom(Font::NORMAL.line_height() * description_lines);
self.title.place(title);
self.loader_y_offset = loader.center().y - constant::screen().center().y;
self.description.place(description);
self.description_pad.place(description);
Self::AREA
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
if let Event::Progress(new_value, new_description) = event {
if mem::replace(&mut self.value, new_value) != new_value {
if !animation_disabled() {
ctx.request_paint();
}
self.description.mutate(ctx, |ctx, para| {
if para.inner_mut().content().as_ref() != new_description {
let new_description = unwrap!((self.update_description)(new_description));
para.inner_mut().update(new_description);
para.change_page(0); // Recompute bounding box.
ctx.request_paint();
self.description_pad.clear();
}
});
}
}
None
}
fn paint(&mut self) {
self.title.paint();
if self.indeterminate {
display::loader_indeterminate(
self.value,
self.loader_y_offset,
theme::FG,
theme::BG,
None,
);
} else {
display::loader(self.value, self.loader_y_offset, theme::FG, theme::BG, None);
}
self.description_pad.paint();
self.description.paint();
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(Self::AREA);
self.title.bounds(sink);
self.description.bounds(sink);
}
}
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for Progress {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("Progress");
}
}

@ -141,6 +141,8 @@ impl Component for ResultAnim {
}
}
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for ResultAnim {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {

@ -1,58 +1,56 @@
use crate::{
micropython::buffer::StrBuffer,
time::Instant,
ui::{
component::{
text::paragraphs::{Paragraph, ParagraphStrType, Paragraphs},
text::paragraphs::{Paragraph, Paragraphs},
Child, Component, ComponentExt, Event, EventCtx, Label, Pad,
},
constant::screen,
display::toif::Icon,
geometry::{Alignment, Insets, LinearPlacement, Point, Rect},
geometry::{Alignment, Insets, LinearPlacement, Rect},
model_tr::{
component::{Button, ButtonMsg, ButtonPos, ResultAnim, ResultAnimMsg},
component::{ButtonPos, ResultAnim, ResultAnimMsg},
theme,
},
},
};
use super::{ButtonController, ButtonControllerMsg, ButtonLayout};
pub enum ResultPopupMsg {
Confirmed,
}
pub struct ResultPopup<S> {
pub struct ResultPopup {
area: Rect,
pad: Pad,
result_anim: Child<ResultAnim>,
headline_baseline: Point,
headline: Option<Label<&'static str>>,
text: Child<Paragraphs<Paragraph<S>>>,
button: Option<Child<Button<&'static str>>>,
text: Child<Paragraphs<Paragraph<StrBuffer>>>,
buttons: Option<Child<ButtonController<StrBuffer>>>,
autoclose: bool,
}
const ANIM_SIZE: i16 = 18;
const BUTTON_HEIGHT: i16 = 13;
const ANIM_SPACE: i16 = 11;
const ANIM_POS: i16 = 32;
const ANIM_POS_ADJ_HEADLINE: i16 = 10;
const ANIM_POS_ADJ_BUTTON: i16 = 6;
impl<S: ParagraphStrType> ResultPopup<S> {
impl ResultPopup {
pub fn new(
icon: Icon,
text: S,
text: StrBuffer,
headline: Option<&'static str>,
button_text: Option<&'static str>,
) -> Self {
let p1 = Paragraphs::new(Paragraph::new(&theme::TEXT_NORMAL, text))
.with_placement(LinearPlacement::vertical().align_at_center());
let button = button_text.map(|t| {
Child::new(Button::with_text(
ButtonPos::Right,
t,
theme::button_default(),
))
let buttons = button_text.map(|text| {
let btn_layout = ButtonLayout::none_none_text(text.into());
Child::new(ButtonController::new(btn_layout))
});
let mut pad = Pad::with_background(theme::BG);
@ -63,9 +61,8 @@ impl<S: ParagraphStrType> ResultPopup<S> {
pad,
result_anim: Child::new(ResultAnim::new(icon)),
headline: headline.map(|a| Label::new(a, Alignment::Center, theme::TEXT_BOLD)),
headline_baseline: Point::zero(),
text: Child::new(p1),
button,
buttons,
autoclose: false,
}
}
@ -78,7 +75,7 @@ impl<S: ParagraphStrType> ResultPopup<S> {
pub fn start(&mut self, ctx: &mut EventCtx) {
self.text.request_complete_repaint(ctx);
self.headline.request_complete_repaint(ctx);
self.button.request_complete_repaint(ctx);
self.buttons.request_complete_repaint(ctx);
self.result_anim.mutate(ctx, |ctx, c| {
let now = Instant::now();
c.start_growing(ctx, now);
@ -87,7 +84,7 @@ impl<S: ParagraphStrType> ResultPopup<S> {
}
}
impl<S: ParagraphStrType> Component for ResultPopup<S> {
impl Component for ResultPopup {
type Msg = ResultPopupMsg;
fn place(&mut self, bounds: Rect) -> Rect {
@ -102,8 +99,8 @@ impl<S: ParagraphStrType> Component for ResultPopup<S> {
headline_height = h.max_size().y;
anim_adjust += ANIM_POS_ADJ_HEADLINE;
}
if self.button.is_some() {
button_height = BUTTON_HEIGHT;
if self.buttons.is_some() {
button_height = theme::BUTTON_HEIGHT;
anim_adjust += ANIM_POS_ADJ_BUTTON;
}
@ -114,7 +111,7 @@ impl<S: ParagraphStrType> Component for ResultPopup<S> {
let (text, buttons) = rest.split_bottom(button_height);
self.pad.place(bounds);
self.button.place(buttons);
self.buttons.place(buttons);
self.headline.place(headline);
self.text.place(text);
self.result_anim
@ -129,12 +126,14 @@ impl<S: ParagraphStrType> Component for ResultPopup<S> {
self.text.event(ctx, event);
self.headline.event(ctx, event);
if let Some(ButtonMsg::Clicked) = self.button.event(ctx, event) {
if let Some(ButtonControllerMsg::Triggered(ButtonPos::Right)) =
self.buttons.event(ctx, event)
{
button_confirmed = true;
}
if let Some(ResultAnimMsg::FullyGrown) = self.result_anim.event(ctx, event) {
if self.button.is_none() || self.autoclose {
if self.buttons.is_none() || self.autoclose {
return Some(ResultPopupMsg::Confirmed);
}
}
@ -149,22 +148,24 @@ impl<S: ParagraphStrType> Component for ResultPopup<S> {
fn paint(&mut self) {
self.pad.paint();
self.text.paint();
self.button.paint();
self.buttons.paint();
self.headline.paint();
self.result_anim.paint();
}
}
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
impl<S: ParagraphStrType> crate::trace::Trace for ResultPopup<S> {
impl crate::trace::Trace for ResultPopup {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("ResultPopup");
t.child("text", &self.text);
if let Some(b) = self.button.as_ref() {
t.child("button", b)
if let Some(button) = &self.buttons {
t.child("button", button);
}
if let Some(h) = self.headline.as_ref() {
t.child("headline", h)
if let Some(headline) = &self.headline {
t.child("headline", headline);
}
t.child("result_anim", &self.result_anim);
}

@ -0,0 +1,249 @@
use crate::ui::{
component::{Component, Event, EventCtx, Never, Pad},
display,
geometry::{Offset, Point, Rect},
model_tr::theme,
};
use heapless::Vec;
/// Scrollbar to be painted horizontally at the top right of the screen.
pub struct ScrollBar {
pad: Pad,
pub page_count: usize,
pub active_page: usize,
}
/// Carrying the appearance of the scrollbar dot.
#[derive(Debug)]
enum DotType {
BigFull, // *
Big, // O
Middle, // o
Small, // .
}
pub const SCROLLBAR_SPACE: i16 = 5;
/// How many dots at most will there be
const MAX_DOTS: usize = 5;
impl ScrollBar {
/// Maximum size (width/height) of a dot
pub const MAX_DOT_SIZE: i16 = 5;
/// Distance between two dots
pub const DOTS_DISTANCE: i16 = 2;
pub const DOTS_INTERVAL: i16 = Self::MAX_DOT_SIZE + Self::DOTS_DISTANCE;
pub const MAX_WIDTH: i16 = Self::DOTS_INTERVAL * MAX_DOTS as i16 - Self::DOTS_DISTANCE;
pub fn new(page_count: usize) -> Self {
Self {
pad: Pad::with_background(theme::BG),
page_count,
active_page: 0,
}
}
/// Page count will be given later as it is not available yet.
pub fn to_be_filled_later() -> Self {
Self::new(0)
}
/// The width the scrollbar will really occupy.
pub fn overall_width(&self) -> i16 {
if self.page_count <= MAX_DOTS {
Self::DOTS_INTERVAL * self.page_count as i16 - Self::DOTS_DISTANCE
} else {
Self::MAX_WIDTH
}
}
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) {
if active_page != self.active_page {
self.active_page = active_page;
}
}
/// 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, dot_type: &DotType, top_right: Point) {
let full_square =
Rect::from_top_right_and_size(top_right, Offset::uniform(Self::MAX_DOT_SIZE));
match dot_type {
DotType::BigFull | DotType::Big => {
// FG - painting the full square
display::rect_fill(full_square, theme::FG);
// BG - erase four corners
display::rect_fill_corners(full_square, theme::BG);
// BG - erasing the middle when not full
if matches!(dot_type, DotType::Big) {
display::rect_fill(full_square.shrink(1), theme::BG)
}
}
DotType::Middle => {
let middle_square = full_square.shrink(1);
// FG - painting the middle square
display::rect_fill(middle_square, theme::FG);
// BG - erase four corners
display::rect_fill_corners(middle_square, theme::BG);
// BG - erasing the middle
display::rect_fill(middle_square.shrink(1), theme::BG)
}
DotType::Small => {
// FG - painting the small square
display::rect_fill(full_square.shrink(2), theme::FG)
}
}
}
/// Get a sequence of dots to be drawn, with specifying their appearance.
/// Painting only big dots in case of 2 and 3 pages,
/// three big and 1 middle in case of 4 pages,
/// and three big, one middle and one small in case of 5 and more pages.
fn get_drawable_dots(&self) -> Vec<DotType, MAX_DOTS> {
let mut dots = Vec::new();
match self.page_count {
0..=3 => {
// *OO
// O*O
// OO*
for i in 0..self.page_count {
if i == self.active_page {
unwrap!(dots.push(DotType::BigFull));
} else {
unwrap!(dots.push(DotType::Big));
}
}
}
4 => {
// *OOo
// O*Oo
// oO*O
// oOO*
match self.active_page {
0 => unwrap!(dots.push(DotType::BigFull)),
1 => unwrap!(dots.push(DotType::Big)),
_ => unwrap!(dots.push(DotType::Middle)),
};
match self.active_page {
1 => unwrap!(dots.push(DotType::BigFull)),
_ => unwrap!(dots.push(DotType::Big)),
};
match self.active_page {
2 => unwrap!(dots.push(DotType::BigFull)),
_ => unwrap!(dots.push(DotType::Big)),
};
match self.active_page {
3 => unwrap!(dots.push(DotType::BigFull)),
2 => unwrap!(dots.push(DotType::Big)),
_ => unwrap!(dots.push(DotType::Middle)),
};
}
_ => {
// *OOo.
// O*Oo.
// oO*Oo
// ...
// oO*Oo
// .oO*O
// .oOO*
let full_dot_index = match self.active_page {
0 => 0,
1 => 1,
last_but_one if last_but_one == self.page_count - 2 => 3,
last if last == self.page_count - 1 => 4,
_ => 2,
};
match full_dot_index {
0 => unwrap!(dots.push(DotType::BigFull)),
1 => unwrap!(dots.push(DotType::Big)),
2 => unwrap!(dots.push(DotType::Middle)),
_ => unwrap!(dots.push(DotType::Small)),
};
match full_dot_index {
0 => unwrap!(dots.push(DotType::Big)),
1 => unwrap!(dots.push(DotType::BigFull)),
2 => unwrap!(dots.push(DotType::Big)),
_ => unwrap!(dots.push(DotType::Middle)),
};
match full_dot_index {
2 => unwrap!(dots.push(DotType::BigFull)),
_ => unwrap!(dots.push(DotType::Big)),
};
match full_dot_index {
0 | 1 => unwrap!(dots.push(DotType::Middle)),
3 => unwrap!(dots.push(DotType::BigFull)),
_ => unwrap!(dots.push(DotType::Big)),
};
match full_dot_index {
0 | 1 => unwrap!(dots.push(DotType::Small)),
2 => unwrap!(dots.push(DotType::Middle)),
3 => unwrap!(dots.push(DotType::Big)),
_ => unwrap!(dots.push(DotType::BigFull)),
};
}
}
dots
}
/// Drawing the dots horizontally and aligning to the right.
fn paint_horizontal(&mut self) {
let mut top_right = self.pad.area.top_right();
for dot in self.get_drawable_dots().iter().rev() {
self.paint_dot(dot, top_right);
top_right.x -= Self::DOTS_INTERVAL;
}
}
}
impl Component for ScrollBar {
type Msg = Never;
fn place(&mut self, bounds: Rect) -> Rect {
// Occupying as little space as possible (according to the number of pages),
// aligning to the right.
let scrollbar_area = Rect::from_top_right_and_size(
bounds.top_right() + Offset::y(1), // offset for centering vertically
Offset::new(self.overall_width(), Self::MAX_DOT_SIZE),
);
self.pad.place(scrollbar_area);
scrollbar_area
}
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();
self.paint_horizontal();
}
}
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for ScrollBar {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("ScrollBar");
t.int("scrollbar_page_count", self.page_count as i64);
t.int("scrollbar_active_page", self.active_page as i64);
}
}

@ -0,0 +1,236 @@
use crate::{
micropython::buffer::StrBuffer,
ui::{
component::{Component, Event, EventCtx, Never, Paginate},
display::{text_multiline_split_words, Font},
geometry::{Alignment, Offset, Rect},
model_tr::theme,
},
};
use crate::ui::{
component::Child,
model_tr::component::{scrollbar::SCROLLBAR_SPACE, title::Title, ScrollBar},
};
use heapless::{String, Vec};
use super::common::display;
const WORDS_PER_PAGE: usize = 3;
const EXTRA_LINE_HEIGHT: i16 = 2;
const NUMBER_X_OFFSET: i16 = 5;
const NUMBER_WORD_OFFSET: i16 = 20;
const NUMBER_FONT: Font = Font::DEMIBOLD;
const WORD_FONT: Font = Font::NORMAL;
const INFO_TOP_OFFSET: i16 = 15;
/// Showing the given share words.
pub struct ShareWords<const N: usize> {
area: Rect,
title: Child<Title>,
scrollbar: Child<ScrollBar>,
share_words: Vec<StrBuffer, N>,
page_index: usize,
}
impl<const N: usize> ShareWords<N> {
pub fn new(title: StrBuffer, share_words: Vec<StrBuffer, N>) -> Self {
let mut instance = Self {
area: Rect::zero(),
title: Child::new(Title::new(title)),
scrollbar: Child::new(ScrollBar::to_be_filled_later()),
share_words,
page_index: 0,
};
let page_count = instance.total_page_count();
let scrollbar = ScrollBar::new(page_count);
instance.scrollbar = Child::new(scrollbar);
instance
}
fn word_index(&self) -> usize {
(self.page_index - 2) * WORDS_PER_PAGE
}
fn is_entry_page(&self) -> bool {
self.page_index == 0
}
fn is_second_page(&self) -> bool {
self.page_index == 1
}
fn is_final_page(&self) -> bool {
self.page_index == self.total_page_count() - 1
}
fn total_page_count(&self) -> usize {
let word_screens = if self.share_words.len() % WORDS_PER_PAGE == 0 {
self.share_words.len() / WORDS_PER_PAGE
} else {
self.share_words.len() / WORDS_PER_PAGE + 1
};
// Two pages before the words, one after it
2 + word_screens + 1
}
fn get_first_text(&self) -> String<100> {
build_string!(
100,
"Write all ",
inttostr!(self.share_words.len() as u8),
" words in order on recovery seed card."
)
}
/// Display the first page with user information.
fn paint_entry_page(&mut self) {
text_multiline_split_words(
self.area.split_top(INFO_TOP_OFFSET).1,
&self.get_first_text(),
Font::BOLD,
theme::FG,
theme::BG,
Alignment::Start,
);
}
fn get_second_text(&self) -> String<100> {
build_string!(100, "Do NOT make digital copies!")
}
/// Display the second page with user information.
fn paint_second_page(&mut self) {
text_multiline_split_words(
self.area.split_top(INFO_TOP_OFFSET).1,
&self.get_second_text(),
Font::MONO,
theme::FG,
theme::BG,
Alignment::Start,
);
}
fn get_final_text(&self) -> String<100> {
build_string!(
100,
"I wrote down all ",
inttostr!(self.share_words.len() as u8),
" words in order."
)
}
/// Display the final page with user confirmation.
fn paint_final_page(&mut self) {
text_multiline_split_words(
self.area.split_top(INFO_TOP_OFFSET).1,
&self.get_final_text(),
Font::MONO,
theme::FG,
theme::BG,
Alignment::Start,
);
}
/// Display current set of recovery words.
fn paint_words(&mut self) {
let mut y_offset = 0;
// Showing the word index and the words itself
for i in 0..WORDS_PER_PAGE {
y_offset += NUMBER_FONT.line_height() + EXTRA_LINE_HEIGHT;
let index = self.word_index() + i;
if index >= self.share_words.len() {
break;
}
let word = &self.share_words[index];
let baseline = self.area.top_left() + Offset::new(NUMBER_X_OFFSET, y_offset);
display(baseline, &inttostr!(index as u8 + 1), NUMBER_FONT);
display(baseline + Offset::x(NUMBER_WORD_OFFSET), &word, WORD_FONT);
}
}
}
impl<const N: usize> Component for ShareWords<N> {
type Msg = Never;
fn place(&mut self, bounds: Rect) -> Rect {
let (title_area, _) = bounds.split_top(theme::FONT_HEADER.line_height());
let (title_area, scrollbar_area) =
title_area.split_right(self.scrollbar.inner().overall_width() + SCROLLBAR_SPACE);
self.title.place(title_area);
self.scrollbar.place(scrollbar_area);
self.area = bounds;
self.area
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
self.title.event(ctx, event);
self.scrollbar.event(ctx, event);
None
}
fn paint(&mut self) {
// Showing scrollbar in all cases
// Individual pages are responsible for not colliding with it
self.scrollbar.paint();
if self.is_entry_page() {
self.title.paint();
self.paint_entry_page();
} else if self.is_second_page() {
self.paint_second_page();
} else if self.is_final_page() {
self.paint_final_page();
} else {
self.paint_words();
}
}
}
impl<const N: usize> Paginate for ShareWords<N> {
fn page_count(&mut self) -> usize {
// Not defining the logic here, as we do not want it to be `&mut`.
self.total_page_count()
}
fn change_page(&mut self, active_page: usize) {
self.page_index = active_page;
self.scrollbar.inner_mut().set_active_page(active_page);
}
}
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
impl<const N: usize> crate::trace::Trace for ShareWords<N> {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("ShareWords");
let content = if self.is_entry_page() {
build_string!(
100,
self.title.inner().get_text(),
"\n",
&self.get_first_text()
)
} else if self.is_second_page() {
self.get_second_text()
} else if self.is_final_page() {
self.get_final_text()
} else {
let mut content = String::<100>::new();
for i in 0..WORDS_PER_PAGE {
let index = self.word_index() + i;
if index >= self.share_words.len() {
break;
}
let word = &self.share_words[index];
let current_line = build_string!(20, inttostr!(index as u8 + 1), " ", word, "\n");
unwrap!(content.push_str(&current_line));
}
content
};
t.string("screen_content", &content);
}
}

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

Loading…
Cancel
Save