TR
63
ci/build.yml
@ -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"
|
||||
|
61
ci/test.yml
@ -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;
|
||||
}
|
||||
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 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
|
||||
/**
|
||||
* 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',
|
||||
|
BIN
core/assets/model_r/amount.png
Normal file
After Width: | Height: | Size: 180 B |
BIN
core/assets/model_r/amount_smaller.png
Normal file
After Width: | Height: | Size: 479 B |
BIN
core/assets/model_r/back_up_arrow.png
Normal file
After Width: | Height: | Size: 133 B |
BIN
core/assets/model_r/bin.png
Normal file
After Width: | Height: | Size: 175 B |
BIN
core/assets/model_r/cancel_for_outline.png
Normal file
After Width: | Height: | Size: 164 B |
BIN
core/assets/model_r/cancel_no_outline.png
Normal file
After Width: | Height: | Size: 125 B |
BIN
core/assets/model_r/delete.png
Normal file
After Width: | Height: | Size: 134 B |
BIN
core/assets/model_r/down_arrow.png
Normal file
After Width: | Height: | Size: 162 B |
BIN
core/assets/model_r/eye.png
Normal file
After Width: | Height: | Size: 129 B |
BIN
core/assets/model_r/eye_round.png
Normal file
After Width: | Height: | Size: 139 B |
BIN
core/assets/model_r/homescreen.png
Normal file
After Width: | Height: | Size: 342 B |
BIN
core/assets/model_r/left_arm.png
Normal file
After Width: | Height: | Size: 154 B |
BIN
core/assets/model_r/left_arrow.png
Normal file
After Width: | Height: | Size: 157 B |
BIN
core/assets/model_r/lock.png
Normal file
After Width: | Height: | Size: 170 B |
BIN
core/assets/model_r/logo_22_33.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
BIN
core/assets/model_r/next_page.png
Normal file
After Width: | Height: | Size: 139 B |
BIN
core/assets/model_r/param.png
Normal file
After Width: | Height: | Size: 159 B |
BIN
core/assets/model_r/param_smaller.png
Normal file
After Width: | Height: | Size: 155 B |
BIN
core/assets/model_r/prev_page.png
Normal file
After Width: | Height: | Size: 137 B |
BIN
core/assets/model_r/right_arm.png
Normal file
After Width: | Height: | Size: 165 B |
BIN
core/assets/model_r/right_arrow.png
Normal file
After Width: | Height: | Size: 156 B |
BIN
core/assets/model_r/right_arrow_fat.png
Normal file
After Width: | Height: | Size: 165 B |
BIN
core/assets/model_r/space.png
Normal file
After Width: | Height: | Size: 118 B |
BIN
core/assets/model_r/tick.png
Normal file
After Width: | Height: | Size: 117 B |
BIN
core/assets/model_r/tick_fat.png
Normal file
After Width: | Height: | Size: 164 B |
BIN
core/assets/model_r/up_arrow.png
Normal file
After Width: | Height: | Size: 166 B |
BIN
core/assets/model_r/user.png
Normal file
After Width: | Height: | Size: 176 B |
BIN
core/assets/model_r/user_smaller.png
Normal file
After Width: | Height: | Size: 131 B |
BIN
core/assets/model_r/wallet.png
Normal file
After Width: | Height: | Size: 178 B |
BIN
core/assets/model_r/warning_left.png
Normal file
After Width: | Height: | Size: 178 B |
BIN
core/assets/model_r/warning_right.png
Normal file
After Width: | Height: | Size: 142 B |
@ -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 };
|
||||
|
203
core/embed/lib/fonts/font_unifont_bold_16.c
Normal file
@ -0,0 +1,203 @@
|
||||
#include <stdint.h>
|
||||
|
||||
// clang-format off
|
||||
|
||||
// - the first two bytes are width and height of the glyph
|
||||
// - the third, fourth and fifth bytes are advance, bearingX and bearingY of the horizontal metrics of the glyph
|
||||
// - the rest is packed 1-bit glyph data
|
||||
|
||||
/* */ static const uint8_t Font_Unifont_Bold_16_glyph_32[] = { 0, 0, 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,
|
||||
};
|
10
core/embed/lib/fonts/font_unifont_bold_16.h
Normal file
@ -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[];
|
207
core/embed/lib/fonts/font_unifont_regular_16.c
Normal file
@ -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,
|
||||
};
|
10
core/embed/lib/fonts/font_unifont_regular_16.h
Normal file
@ -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)) {}
|
||||
}
|
||||
|
270
core/embed/rust/src/ui/model_tr/component/address_details.rs
Normal file
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
498
core/embed/rust/src/ui/model_tr/component/button_controller.rs
Normal file
@ -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);
|
||||
}
|
||||
}
|
149
core/embed/rust/src/ui/model_tr/component/changing_text.rs
Normal file
@ -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());
|
||||
}
|
||||
}
|
28
core/embed/rust/src/ui/model_tr/component/common.rs
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
329
core/embed/rust/src/ui/model_tr/component/flow.rs
Normal file
@ -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);
|
||||
}
|
||||
}
|
355
core/embed/rust/src/ui/model_tr/component/flow_pages.rs
Normal file
@ -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));
|
||||
});
|
||||
}
|
||||
}
|
171
core/embed/rust/src/ui/model_tr/component/flow_pages_helpers.rs
Normal file
@ -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,25 +1,112 @@
|
||||
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> Component for Frame<T>
|
||||
where
|
||||
T: Component,
|
||||
{
|
||||
type Msg = T::Msg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
const TITLE_SPACE: i16 = 2;
|
||||
|
||||
let (title_area, content_area) = bounds.split_top(theme::FONT_HEADER.line_height());
|
||||
let content_area = content_area.inset(Insets::top(TITLE_SPACE));
|
||||
|
||||
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) {
|
||||
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),
|
||||
}
|
||||
}
|
||||
@ -27,52 +114,101 @@ where
|
||||
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, U> Component for Frame<T, U>
|
||||
impl<T> Component for ScrollableFrame<T>
|
||||
where
|
||||
T: Component,
|
||||
U: AsRef<str>,
|
||||
T: Component + ScrollableContent,
|
||||
{
|
||||
type Msg = T::Msg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
const TITLE_SPACE: i16 = 4;
|
||||
// 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_area, content_area) = bounds.split_top(Font::BOLD.line_height());
|
||||
let content_area = content_area.inset(Insets::top(TITLE_SPACE));
|
||||
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.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> {
|
||||
self.content.event(ctx, event)
|
||||
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) {
|
||||
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.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);
|
||||
}
|
||||
}
|
||||
|
126
core/embed/rust/src/ui/model_tr/component/hold_to_confirm.rs
Normal file
@ -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);
|
||||
}
|
||||
}
|
233
core/embed/rust/src/ui/model_tr/component/homescreen.rs
Normal file
@ -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, ¬ification.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);
|
||||
}
|
||||
}
|
257
core/embed/rust/src/ui/model_tr/component/input_methods/pin.rs
Normal file
@ -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 result_anim;
|
||||
mod result_popup;
|
||||
mod welcome_screen;
|
||||
|
||||
use super::theme;
|
||||
|
||||
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 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 page::ButtonPage;
|
||||
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;
|
||||
|
||||
#[cfg(feature = "micropython")]
|
||||
pub use address_details::{AddressDetails, AddressDetailsMsg};
|
||||
|
||||
#[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;
|
||||
#[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};
|
||||
|
70
core/embed/rust/src/ui/model_tr/component/no_btn_dialog.rs
Normal file
@ -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()),
|
||||
}
|
||||
}
|
||||
|
||||
fn change_page(&mut self, ctx: &mut EventCtx, page: usize) {
|
||||
// Change the page in the content, clear the background under it and make sure
|
||||
// it gets completely repainted.
|
||||
self.content.change_page(page);
|
||||
self.content.request_complete_repaint(ctx);
|
||||
self.pad.clear();
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Component for ButtonPage<T>
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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,
|
||||
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())),
|
||||
}
|
||||
}
|
||||
|
||||
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_cancel_btn(mut self, btn_details: Option<ButtonDetails<StrBuffer>>) -> Self {
|
||||
self.cancel_btn_details = btn_details;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_confirm_btn(mut self, btn_details: Option<ButtonDetails<StrBuffer>>) -> Self {
|
||||
self.confirm_btn_details = btn_details;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_back_btn(mut self, btn_details: Option<ButtonDetails<StrBuffer>>) -> Self {
|
||||
self.back_btn_details = btn_details;
|
||||
self
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
};
|
||||
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
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
131
core/embed/rust/src/ui/model_tr/component/progress.rs
Normal file
@ -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);
|
||||
}
|
||||
|
249
core/embed/rust/src/ui/model_tr/component/scrollbar.rs
Normal file
@ -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);
|
||||
}
|
||||
}
|
236
core/embed/rust/src/ui/model_tr/component/share_words.rs
Normal file
@ -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(¤t_line));
|
||||
}
|
||||
content
|
||||
};
|
||||
t.string("screen_content", &content);
|
||||
}
|
||||
}
|