feat(all): add UI for Model R

pull/3048/head
grdddj 1 year ago committed by Jiří Musil
parent 92045275fb
commit da14c22712

@ -133,6 +133,37 @@ core fw btconly production build:
- firmware-T2T1-btconly-production-*.*.*-$CI_COMMIT_SHORT_SHA.bin
expire_in: 1 week
core fw R debug build:
stage: build
<<: *gitlab_caching
needs: []
variables:
TREZOR_MODEL: "R"
PYOPT: "0"
script:
- nix-shell --run "poetry run make -C core build_firmware"
- cp core/build/firmware/firmware.bin trezor-fw-debug-tr-$CORE_VERSION-$CI_COMMIT_SHORT_SHA.bin
artifacts:
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
paths:
- trezor-fw-*.*.*-$CI_COMMIT_SHORT_SHA.bin
expire_in: 1 week
core fw R build:
stage: build
<<: *gitlab_caching
needs: []
variables:
TREZOR_MODEL: "R"
script:
- nix-shell --run "poetry run make -C core build_firmware"
- cp core/build/firmware/firmware.bin trezor-fw-tr-$CORE_VERSION-$CI_COMMIT_SHORT_SHA.bin
artifacts:
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
paths:
- trezor-fw-*.*.*-$CI_COMMIT_SHORT_SHA.bin
expire_in: 1 week
# Non-frozen emulator build. This means you still need Python files
# present which get interpreted.
core unix regular build:
@ -235,6 +266,38 @@ core unix frozen debug build:
untracked: true
expire_in: 1 week
core unix frozen R debug build:
stage: build
<<: *gitlab_caching
needs: []
variables:
PYOPT: "0"
TREZOR_MODEL: "R"
script:
- nix-shell --run "poetry run make -C core build_unix_frozen"
artifacts:
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
untracked: true
expire_in: 10 weeks
core unix frozen R debug build arm:
image: nixos/nix
stage: build
<<: *gitlab_caching
needs: []
variables:
PYOPT: "0"
TREZOR_MODEL: "R"
script:
- nix-shell --run "poetry run make -C core build_unix_frozen"
- mv core/build/unix/trezor-emu-core core/build/unix/trezor-emu-core-arm
artifacts:
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
untracked: true
expire_in: 10 weeks
tags:
- docker_darwin_arm
core unix frozen debug asan build:
stage: build
<<: *gitlab_caching

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

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

@ -39,10 +39,21 @@ message DebugLinkDecision {
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
}
/**

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

@ -0,0 +1 @@
Implement UI for Model R

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 B

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

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

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

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

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

@ -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
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 = [
"button",
"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;
@ -89,6 +95,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 +114,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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 KiB

@ -26,6 +26,13 @@ pub enum Error {
ValueErrorParam(&'static CStr, Obj),
}
#[macro_export]
macro_rules! value_error {
($msg:expr) => {
Error::ValueError(cstr!($msg))
};
}
#[cfg(feature = "micropython")]
impl Error {
/// Create an exception instance matching the error code. The result of this

@ -11,6 +11,7 @@
#[macro_use]
extern crate num_derive;
#[macro_use]
mod error;
// use trezorhal for its macros early
#[macro_use]

@ -1,6 +1,10 @@
use core::{convert::TryFrom, ops::Deref, ptr, slice, str};
use crate::{error::Error, micropython::obj::Obj, strutil::hexlify};
use crate::{
error::Error,
micropython::obj::Obj,
strutil::{hexlify, SkipPrefix},
};
use super::ffi;
@ -20,7 +24,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,
@ -89,8 +93,10 @@ impl StrBuffer {
unsafe { slice::from_raw_parts(self.ptr.add(self.off.into()), self.len.into()) }
}
}
}
pub fn offset(&self, skip_bytes: usize) -> Self {
impl SkipPrefix for StrBuffer {
fn skip_prefix(&self, skip_bytes: usize) -> Self {
let off: u16 = unwrap!(skip_bytes.try_into());
assert!(off <= self.len);
assert!(self.as_ref().is_char_boundary(skip_bytes));
@ -251,5 +257,5 @@ pub fn hexlify_bytes(obj: Obj, offset: usize, max_len: usize) -> Result<StrBuffe
let max_len = max_len & !1;
let hex_len = (bin_slice.len() * 2).min(max_len);
let result = StrBuffer::alloc_with(hex_len, move |buffer| hexlify(bin_slice, buffer))?;
Ok(result.offset(hex_off))
Ok(result.skip_prefix(hex_off))
}

@ -303,6 +303,23 @@ impl TryFrom<(Obj, Obj)> for Obj {
}
}
impl TryFrom<(Obj, Obj, Obj)> for Obj {
type Error = Error;
fn try_from(val: (Obj, Obj, Obj)) -> Result<Self, Self::Error> {
// SAFETY:
// - Should work with any micropython objects.
// EXCEPTION: Will raise if allocation fails.
let values = [val.0, val.1, val.2];
let obj = catch_exception(|| unsafe { ffi::mp_obj_new_tuple(3, values.as_ptr()) })?;
if obj.is_null() {
Err(Error::AllocationFailed)
} else {
Ok(obj)
}
}
}
//
// # Additional conversions based on the methods above.
//

@ -1,31 +1,25 @@
use cstr_core::CStr;
use cstr_core::cstr;
use crate::{error::Error, micropython::qstr::Qstr};
// XXX const version of `from_bytes_with_nul_unchecked` is nightly-only.
pub fn experimental_not_enabled() -> Error {
let msg =
unsafe { CStr::from_bytes_with_nul_unchecked(b"Experimental features are disabled.\0") };
Error::ValueError(msg)
value_error!("Experimental features are disabled.")
}
pub fn unknown_field_type() -> Error {
let msg = unsafe { CStr::from_bytes_with_nul_unchecked(b"Unknown field type.\0") };
Error::ValueError(msg)
value_error!("Unknown field type.")
}
pub fn missing_required_field(field: Qstr) -> Error {
let msg = unsafe { CStr::from_bytes_with_nul_unchecked(b"Missing required field\0") };
Error::ValueErrorParam(msg, field.into())
Error::ValueErrorParam(cstr!("Missing required field."), field.into())
}
pub fn invalid_value(field: Qstr) -> Error {
let msg = unsafe { CStr::from_bytes_with_nul_unchecked(b"Invalid value for field\0") };
Error::ValueErrorParam(msg, field.into())
Error::ValueErrorParam(cstr!("Invalid value for field."), field.into())
}
pub fn end_of_buffer() -> Error {
let msg = unsafe { CStr::from_bytes_with_nul_unchecked(b"End of buffer.\0") };
Error::ValueError(msg)
value_error!("End of buffer.")
}

@ -1,6 +1,6 @@
#![allow(dead_code)]
use crate::trezorhal::storage::{get, get_length};
use crate::trezorhal::storage::{get, get_length, StorageResult};
pub const HOMESCREEN_MAX_SIZE: usize = 16384;
@ -36,26 +36,13 @@ const INITIALIZED: u16 = FLAG_PUBLIC | APP_DEVICE | 0x0013;
const SAFETY_CHECK_LEVEL: u16 = APP_DEVICE | 0x0014;
const EXPERIMENTAL_FEATURES: u16 = APP_DEVICE | 0x0015;
pub fn get_avatar_len() -> Result<usize, ()> {
let avatar_len_res = get_length(HOMESCREEN);
if let Ok(len) = avatar_len_res {
Ok(len)
} else {
Err(())
}
pub fn get_avatar_len() -> StorageResult<usize> {
get_length(HOMESCREEN)
}
pub fn get_avatar(buffer: &mut [u8]) -> Result<usize, ()> {
let avatar_len_res = get_length(HOMESCREEN);
if let Ok(len) = avatar_len_res {
if len <= buffer.len() {
unwrap!(get(HOMESCREEN, buffer));
Ok(len)
} else {
Err(())
}
} else {
Err(())
}
pub fn load_avatar(dest: &mut [u8]) -> StorageResult<()> {
let dest_len = dest.len();
let result = get(HOMESCREEN, dest)?;
ensure!(dest_len == result.len(), "Internal error in load_avatar");
Ok(())
}

@ -1,3 +1,35 @@
use heapless::String;
/// Trait for slicing off string prefix by a specified number of bytes.
/// See `StringType` for deeper explanation.
pub trait SkipPrefix {
fn skip_prefix(&self, bytes: usize) -> Self;
}
// XXX only implemented in bootloader, as we don't want &str to satisfy
// StringType in the main firmware. This is because we want to avoid duplication
// of every StringType-parametrized component.
#[cfg(feature = "bootloader")]
impl SkipPrefix for &str {
fn skip_prefix(&self, chars: usize) -> Self {
&self[chars..]
}
}
/// Trait for internal representation of strings.
/// Exists so that we can support `StrBuffer` as well as `&str` in the UI
/// components. Implies the following operations:
/// - dereference into a short-lived `&str` reference (AsRef<str>)
/// - create a new string by skipping some number of bytes (SkipPrefix) - used
/// when rendering continuations of long strings
/// - create a new string from a string literal (From<&'static str>)
pub trait StringType: AsRef<str> + From<&'static str> + SkipPrefix {}
impl<T> StringType for T where T: AsRef<str> + From<&'static str> + SkipPrefix {}
/// Unified-length String type, long enough for most simple use-cases.
pub type ShortString = String<50>;
pub fn hexlify(data: &[u8], buffer: &mut [u8]) {
const HEX_LOWER: [u8; 16] = *b"0123456789abcdef";
let mut i: usize = 0;

@ -5,6 +5,7 @@ pub trait Tracer {
fn int(&mut self, key: &str, i: i64);
fn string(&mut self, key: &str, s: &str);
fn bool(&mut self, key: &str, b: bool);
fn null(&mut self, key: &str);
fn in_child(&mut self, key: &str, block: &dyn Fn(&mut dyn Tracer));
fn in_list(&mut self, key: &str, block: &dyn Fn(&mut dyn ListTracer));
@ -169,6 +170,11 @@ impl<F: FnMut(&str)> Tracer for JsonTracer<F> {
(self.write_fn)(if b { "true" } else { "false" });
}
fn null(&mut self, key: &str) {
self.key(key);
(self.write_fn)("null");
}
fn in_child(&mut self, key: &str, block: &dyn Fn(&mut dyn Tracer)) {
self.key(key);
(self.write_fn)("{");

@ -9,3 +9,42 @@ 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 {
assert!(max > min);
// 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);
}
}
}

@ -64,12 +64,12 @@ pub enum StorageError {
impl From<StorageError> for Error {
fn from(err: StorageError) -> Self {
match err {
StorageError::InvalidData => Error::ValueError(cstr!("Invalid data for storage")),
StorageError::WriteFailed => Error::ValueError(cstr!("Storage write failed")),
StorageError::ReadFailed => Error::ValueError(cstr!("Storage read failed")),
StorageError::DeleteFailed => Error::ValueError(cstr!("Storage delete failed")),
StorageError::InvalidData => value_error!("Invalid data for storage"),
StorageError::WriteFailed => value_error!("Storage write failed"),
StorageError::ReadFailed => value_error!("Storage read failed"),
StorageError::DeleteFailed => value_error!("Storage delete failed"),
StorageError::CounterFailed => {
Error::ValueError(cstr!("Retrieving counter value failed"))
value_error!("Retrieving counter value failed")
}
}
}

@ -4,56 +4,76 @@ use cstr_core::CStr;
/// Holds all the possible words with the possibility to interact
/// with the "list" - filtering it further, getting their count, etc.
pub struct Wordlist(&'static [*const cty::c_char]);
pub struct Wordlist {
words: &'static [*const cty::c_char],
/// Holds the length of prefix which was used to filter the list
/// (how many beginning characters are common for all words).
prefix_len: usize,
}
impl Wordlist {
pub fn new(words: &'static [*const cty::c_char], prefix_len: usize) -> Self {
Self { words, prefix_len }
}
/// Initialize BIP39 wordlist.
pub fn bip39() -> Self {
Self(unsafe { &ffi::BIP39_WORDLIST_ENGLISH })
Self::new(unsafe { &ffi::BIP39_WORDLIST_ENGLISH }, 0)
}
/// Initialize SLIP39 wordlist.
pub fn slip39() -> Self {
Self(unsafe { &ffi::SLIP39_WORDLIST })
Self::new(unsafe { &ffi::SLIP39_WORDLIST }, 0)
}
/// Returns all possible letters from current wordlist that form a valid
/// word. Alphabetically sorted.
pub fn get_available_letters(&self) -> impl Iterator<Item = char> {
let mut prev_char = '\0';
let prefix_len = self.prefix_len;
self.iter().filter_map(move |word| {
if word.len() <= prefix_len {
return None;
}
let following_char = unwrap!(word.chars().nth(prefix_len));
if following_char != prev_char {
prev_char = following_char;
Some(following_char)
} else {
None
}
})
}
/// Only leaves words that have a specified prefix. Throw away others.
pub fn filter_prefix(&self, prefix: &str) -> Self {
let mut start = 0usize;
let mut end = self.0.len();
for (i, word) in self.0.iter().enumerate() {
// SAFETY: We assume our slice is an array of 0-terminated strings.
match unsafe { prefix_cmp(prefix, *word) } {
Ordering::Less => {
start = i + 1;
}
Ordering::Greater => {
end = i;
break;
}
_ => {}
}
}
Self(&self.0[start..end])
// SAFETY: We assume our slice is an array of 0-terminated strings.
let start = self
.words
.partition_point(|&word| matches!(unsafe { prefix_cmp(prefix, word) }, Ordering::Less));
let end = self.words.partition_point(|&word| {
!matches!(unsafe { prefix_cmp(prefix, word) }, Ordering::Greater)
});
Self::new(&self.words[start..end], prefix.len())
}
/// Get a word at the certain position.
pub fn get(&self, index: usize) -> Option<&'static str> {
// SAFETY: we assume every word in the wordlist is a valid 0-terminated UTF-8
// string.
self.0
self.words
.get(index)
.map(|word| unsafe { from_utf8_unchecked(*word) })
}
/// How many words are currently in the list.
pub const fn len(&self) -> usize {
self.0.len()
self.words.len()
}
/// Iterator of all current words.
pub fn iter(&self) -> impl Iterator<Item = &'static str> {
self.0
self.words
.iter()
.map(|word| unsafe { from_utf8_unchecked(*word) })
}
@ -158,4 +178,36 @@ mod tests {
.collect::<Vec<_>>();
assert_eq!(result, expected_result);
}
#[test]
fn test_get_available_letters() {
let result = Wordlist::bip39()
.filter_prefix("ab")
.get_available_letters()
.collect::<Vec<_>>();
let expected_result = vec!['a', 'i', 'l', 'o', 's', 'u'];
assert_eq!(result, expected_result);
let result = Wordlist::bip39()
.filter_prefix("str")
.get_available_letters()
.collect::<Vec<_>>();
let expected_result = vec!['a', 'e', 'i', 'o', 'u'];
assert_eq!(result, expected_result);
let result = Wordlist::bip39()
.filter_prefix("zoo")
.get_available_letters()
.collect::<Vec<_>>();
let expected_result = vec![];
assert_eq!(result, expected_result);
let result = Wordlist::bip39()
.get_available_letters()
.collect::<Vec<_>>();
let expected_result = vec![
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q',
'r', 's', 't', 'u', 'v', 'w', 'y', 'z',
];
assert_eq!(result, expected_result);
}
}

@ -17,6 +17,8 @@ use crate::ui::event::ButtonEvent;
use crate::ui::event::TouchEvent;
use crate::ui::event::USBEvent;
use super::Paginate;
/// Type used by components that do not return any messages.
///
/// Alternative to the yet-unstable `!`-type.
@ -153,6 +155,16 @@ where
}
}
impl<T: Paginate> Paginate for Child<T> {
fn page_count(&mut self) -> usize {
self.component.page_count()
}
fn change_page(&mut self, active_page: usize) {
self.component.change_page(active_page);
}
}
impl<T> PaintOverlapping for Child<T>
where
T: PaintOverlapping,

@ -73,6 +73,21 @@ where
.fit_text(self.text.as_ref())
.height()
}
pub fn text_area(&self) -> Rect {
// XXX only works on single-line labels
assert!(self.layout.bounds.height() <= self.font().text_max_height());
let available_width = self.layout.bounds.width();
let width = self.font().text_width(self.text.as_ref());
let height = self.font().text_height();
let cursor = self.layout.initial_cursor();
let baseline = match self.alignment() {
Alignment::Start => cursor,
Alignment::Center => cursor + Offset::x(available_width / 2) - Offset::x(width / 2),
Alignment::End => cursor + Offset::x(available_width) - Offset::x(width),
};
Rect::from_bottom_left_and_size(baseline, Offset::new(width, height))
}
}
impl<T> Component for Label<T>

@ -11,6 +11,8 @@ use crate::{
};
const MILLIS_PER_LETTER_M: u32 = 300;
const ANIMATION_DURATION_MS: u32 = 2000;
const PAUSE_DURATION_MS: u32 = 1000;
enum State {
Initial,
@ -49,11 +51,15 @@ where
font,
fg,
bg,
duration: Duration::from_millis(2000),
pause: Duration::from_millis(1000),
duration: Duration::from_millis(ANIMATION_DURATION_MS),
pause: Duration::from_millis(PAUSE_DURATION_MS),
}
}
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() {

@ -32,4 +32,4 @@ pub use text::{
formatted::FormattedText,
layout::{LineBreaking, PageBreaking, TextLayout},
};
pub use timeout::{Timeout, TimeoutMsg};
pub use timeout::Timeout;

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

@ -1,133 +1,103 @@
use core::{
iter::{Enumerate, Peekable},
slice,
use crate::{
strutil::StringType,
ui::{
component::{Component, Event, EventCtx, Never, Paginate},
geometry::Rect,
},
};
use heapless::LinearMap;
use crate::ui::{
component::{Component, Event, EventCtx, Never},
display::{Color, Font},
geometry::Rect,
};
use super::layout::{
LayoutFit, LayoutSink, LineBreaking, Op, PageBreaking, TextLayout, TextRenderer, TextStyle,
use super::{
layout::{LayoutFit, LayoutSink, TextNoOp, TextRenderer},
op::OpTextLayout,
};
pub const MAX_ARGUMENTS: usize = 6;
pub struct FormattedText<F, T> {
layout: TextLayout,
fonts: FormattedFonts,
format: F,
args: LinearMap<&'static str, T, MAX_ARGUMENTS>,
#[derive(Clone)]
pub struct FormattedText<T: StringType + Clone> {
op_layout: OpTextLayout<T>,
char_offset: usize,
}
pub struct FormattedFonts {
/// Font used to format `{normal}`.
pub normal: Font,
/// Font used to format `{demibold}`.
pub demibold: Font,
/// Font used to format `{bold}`.
pub bold: Font,
/// Font used to format `{mono}`.
pub mono: Font,
}
impl<F, T> FormattedText<F, T> {
pub fn new(style: TextStyle, fonts: FormattedFonts, format: F) -> Self {
impl<T: StringType + Clone> FormattedText<T> {
pub fn new(op_layout: OpTextLayout<T>) -> Self {
Self {
format,
fonts,
layout: TextLayout::new(style),
args: LinearMap::new(),
op_layout,
char_offset: 0,
}
}
pub fn with(mut self, key: &'static str, value: T) -> Self {
if self.args.insert(key, value).is_err() {
#[cfg(feature = "ui_debug")]
panic!("text args map is full");
}
self
}
pub fn with_format(mut self, format: F) -> Self {
self.format = format;
self
fn layout_content(&mut self, sink: &mut dyn LayoutSink) -> LayoutFit {
self.op_layout.layout_content(self.char_offset, sink)
}
}
pub fn with_text_font(mut self, text_font: Font) -> Self {
self.layout.style.text_font = text_font;
self
}
// Pagination
impl<T: StringType + Clone> Paginate for FormattedText<T> {
fn page_count(&mut self) -> usize {
let mut page_count = 1; // There's always at least one page.
let mut char_offset = 0;
pub fn with_text_color(mut self, text_color: Color) -> Self {
self.layout.style.text_color = text_color;
self
}
// Make sure we're starting from the beginning.
self.char_offset = char_offset;
pub fn with_line_breaking(mut self, line_breaking: LineBreaking) -> Self {
self.layout.style.line_breaking = line_breaking;
self
}
// 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.char_offset = char_offset;
}
}
}
pub fn with_page_breaking(mut self, page_breaking: PageBreaking) -> Self {
self.layout.style.page_breaking = page_breaking;
self
}
// Reset the char offset back to the beginning.
self.char_offset = 0;
pub fn set_char_offset(&mut self, char_offset: usize) {
self.char_offset = char_offset;
page_count
}
pub fn char_offset(&mut self) -> usize {
self.char_offset
}
fn change_page(&mut self, to_page: usize) {
let mut active_page = 0;
let mut char_offset = 0;
pub fn layout_mut(&mut self) -> &mut TextLayout {
&mut self.layout
}
}
// Make sure we're starting from the beginning.
self.char_offset = char_offset;
impl<F, T> FormattedText<F, T>
where
F: AsRef<str>,
T: AsRef<str>,
{
pub fn layout_content(&self, sink: &mut dyn LayoutSink) -> LayoutFit {
let mut cursor = self.layout.initial_cursor();
let mut ops = Op::skip_n_text_bytes(
Tokenizer::new(self.format.as_ref()).flat_map(|arg| match arg {
Token::Literal(literal) => Some(Op::Text(literal)),
Token::Argument("mono") => Some(Op::Font(self.fonts.mono)),
Token::Argument("bold") => Some(Op::Font(self.fonts.bold)),
Token::Argument("normal") => Some(Op::Font(self.fonts.normal)),
Token::Argument("demibold") => Some(Op::Font(self.fonts.demibold)),
Token::Argument(argument) => self
.args
.get(argument)
.map(|value| Op::Text(value.as_ref())),
}),
self.char_offset,
);
self.layout.layout_ops(&mut ops, &mut cursor, sink)
// 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.char_offset = char_offset;
}
}
}
}
}
impl<F, T> Component for FormattedText<F, T>
where
F: AsRef<str>,
T: AsRef<str>,
{
impl<T: StringType + Clone> Component for FormattedText<T> {
type Msg = Never;
fn place(&mut self, bounds: Rect) -> Rect {
self.layout.bounds = bounds;
self.layout.bounds
self.op_layout.place(bounds);
bounds
}
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
@ -140,133 +110,37 @@ where
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(self.layout.bounds)
sink(self.op_layout.layout.bounds)
}
}
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
impl<F, T> crate::trace::Trace for FormattedText<F, T>
where
F: AsRef<str>,
T: AsRef<str>,
{
impl<T: StringType + Clone> FormattedText<T> {
/// Is the same as layout_content, but does not use `&mut self`
/// to be compatible with `trace`.
/// Therefore it has to do the `clone` of `op_layout`.
pub fn layout_content_debug(&self, sink: &mut dyn LayoutSink) -> LayoutFit {
// TODO: how to solve it "properly", without the `clone`?
// (changing `trace` to `&mut self` had some other isses...)
self.op_layout
.clone()
.layout_content(self.char_offset, sink)
}
}
#[cfg(feature = "ui_debug")]
impl<T: StringType + Clone> crate::trace::Trace for FormattedText<T> {
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("FormattedText");
t.in_list("text", &|l| {
let result = self.layout_content(&mut TraceSink(l));
let result = self.layout_content_debug(&mut TraceSink(l));
fit.set(Some(result));
});
t.bool("fits", matches!(fit.get(), Some(LayoutFit::Fitting { .. })));
}
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum Token<'a> {
/// Process literal text content.
Literal(&'a str),
/// Process argument with specified descriptor.
Argument(&'a str),
}
/// Processes a format string into an iterator of `Token`s.
///
/// # Example
///
/// ```
/// let parser = Tokenizer::new("Nice to meet {you}, where you been?");
/// assert!(matches!(parser.next(), Some(Token::Literal("Nice to meet "))));
/// assert!(matches!(parser.next(), Some(Token::Argument("you"))));
/// assert!(matches!(parser.next(), Some(Token::Literal(", where you been?"))));
/// ```
pub struct Tokenizer<'a> {
input: &'a str,
inner: Peekable<Enumerate<slice::Iter<'a, u8>>>,
}
impl<'a> Tokenizer<'a> {
/// Create a new tokenizer for bytes of a formatting string `input`,
/// returning an iterator.
pub fn new(input: &'a str) -> Self {
assert!(input.is_ascii());
Self {
input,
inner: input.as_bytes().iter().enumerate().peekable(),
}
}
}
impl<'a> Iterator for Tokenizer<'a> {
type Item = Token<'a>;
fn next(&mut self) -> Option<Self::Item> {
const ASCII_OPEN_BRACE: u8 = b'{';
const ASCII_CLOSED_BRACE: u8 = b'}';
match self.inner.next() {
// Argument token is starting. Read until we find '}', then parse the content between
// the braces and return the token. If we encounter the end of string before the closing
// brace, quit.
Some((open, &ASCII_OPEN_BRACE)) => loop {
match self.inner.next() {
Some((close, &ASCII_CLOSED_BRACE)) => {
break Some(Token::Argument(&self.input[open + 1..close]));
}
None => {
break None;
}
_ => {}
}
},
// Literal token is starting. Read until we find '{' or the end of string, and return
// the token. Use `peek()` for matching the opening brace, se we can keep it
// in the iterator for the above code.
Some((start, _)) => loop {
match self.inner.peek() {
Some(&(open, &ASCII_OPEN_BRACE)) => {
break Some(Token::Literal(&self.input[start..open]));
}
None => {
break Some(Token::Literal(&self.input[start..]));
}
_ => {
self.inner.next();
}
}
},
None => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tokenizer_yields_expected_tokens() {
assert!(Tokenizer::new("").eq([]));
assert!(Tokenizer::new("x").eq([Token::Literal("x")]));
assert!(Tokenizer::new("x\0y").eq([Token::Literal("x\0y")]));
assert!(Tokenizer::new("{").eq([]));
assert!(Tokenizer::new("x{").eq([Token::Literal("x")]));
assert!(Tokenizer::new("x{y").eq([Token::Literal("x")]));
assert!(Tokenizer::new("{}").eq([Token::Argument("")]));
assert!(Tokenizer::new("x{}y{").eq([
Token::Literal("x"),
Token::Argument(""),
Token::Literal("y"),
]));
assert!(Tokenizer::new("{\0}").eq([Token::Argument("\0"),]));
assert!(Tokenizer::new("{{y}").eq([Token::Argument("{y"),]));
assert!(Tokenizer::new("{{{{xyz").eq([]));
assert!(Tokenizer::new("x{}{{}}}}").eq([
Token::Literal("x"),
Token::Argument(""),
Token::Argument("{"),
Token::Literal("}}}"),
]));
}
}

@ -175,51 +175,8 @@ impl TextLayout {
}
/// Draw as much text as possible on the current screen.
pub fn render_text(&self, text: &str) {
self.layout_text(text, &mut self.initial_cursor(), &mut TextRenderer);
}
pub fn layout_ops(
mut self,
ops: &mut dyn Iterator<Item = Op<'_>>,
cursor: &mut Point,
sink: &mut dyn LayoutSink,
) -> LayoutFit {
let init_cursor = *cursor;
let mut total_processed_chars = 0;
for op in ops {
match op {
Op::Color(color) => {
self.style.text_color = color;
}
Op::Font(font) => {
self.style.text_font = font;
}
Op::Text(text) => match self.layout_text(text, cursor, sink) {
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),
}
pub fn render_text(&self, text: &str) -> LayoutFit {
self.layout_text(text, &mut self.initial_cursor(), &mut TextRenderer)
}
/// Loop through the `text` and try to fit it on the current screen,
@ -343,7 +300,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)
@ -507,38 +464,6 @@ pub mod trace {
}
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum Op<'a> {
/// Render text with current color and font.
Text(&'a str),
/// Set current text color.
Color(Color),
/// Set currently used font.
Font(Font),
}
impl<'a> Op<'a> {
pub fn skip_n_text_bytes(
ops: impl Iterator<Item = Op<'a>>,
skip_bytes: usize,
) -> impl Iterator<Item = Op<'a>> {
let mut skipped = 0;
ops.filter_map(move |op| match op {
Op::Text(text) if skipped < skip_bytes => {
skipped = skipped.saturating_add(text.len());
if skipped > skip_bytes {
let leave_bytes = skipped - skip_bytes;
Some(Op::Text(&text[text.len() - leave_bytes..]))
} else {
None
}
}
op_to_pass_through => Some(op_to_pass_through),
})
}
}
/// Carries info about the content that was processed
/// on the current line.
#[derive(Debug, PartialEq, Eq)]
@ -557,7 +482,7 @@ struct Span {
}
impl Span {
fn fit_horizontally(
pub fn fit_horizontally(
text: &str,
max_width: i16,
text_font: impl GlyphMetrics,

@ -1,6 +1,8 @@
pub mod common;
pub mod formatted;
pub mod layout;
pub mod op;
pub mod paragraphs;
pub mod util;
pub use layout::{LineBreaking, PageBreaking, TextStyle};

@ -0,0 +1,240 @@
use crate::{
strutil::StringType,
ui::{
display::{Color, Font},
geometry::{Alignment, Offset, Rect},
util::ResultExt,
},
};
use super::{
layout::{LayoutFit, LayoutSink, TextLayout},
LineBreaking, TextStyle,
};
use heapless::Vec;
// So that there is only one implementation, and not multiple generic ones
// as would be via `const N: usize` generics.
const MAX_OPS: usize = 15;
/// 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;
#[derive(Clone)]
/// Extension of TextLayout, allowing for Op-based operations
pub struct OpTextLayout<T: StringType + Clone> {
pub layout: TextLayout,
ops: Vec<Op<T>, MAX_OPS>,
}
impl<'a, T: StringType + Clone + 'a> OpTextLayout<T> {
pub fn new(style: TextStyle) -> Self {
Self {
layout: TextLayout::new(style),
ops: Vec::new(),
}
}
pub fn place(&mut self, bounds: Rect) -> Rect {
self.layout.bounds = bounds;
bounds
}
/// Send the layout's content into a sink.
pub fn layout_content(&mut self, skip_bytes: usize, sink: &mut dyn LayoutSink) -> LayoutFit {
self.layout_ops(skip_bytes, sink)
}
/// 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.
fn layout_ops(&mut self, skip_bytes: usize, sink: &mut dyn LayoutSink) -> LayoutFit {
// TODO: make sure it is called when we have the current font (not sooner)
let mut cursor = &mut self.layout.initial_cursor();
let init_cursor = *cursor;
let mut total_processed_chars = 0;
// Do something when it was not skipped
for op in Self::filter_skipped_ops(self.ops.iter(), skip_bytes) {
match op {
// Changing color
Op::Color(color) => {
self.layout.style.text_color = color;
}
// Changing font
Op::Font(font) => {
self.layout.style.text_font = font;
}
// Changing line/text alignment
Op::Alignment(line_alignment) => {
self.layout.align = line_alignment;
}
// Changing line breaking
Op::LineBreaking(line_breaking) => {
self.layout.style.line_breaking = line_breaking;
}
// 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.layout_height(init_cursor, *cursor),
};
}
// Drawing text
Op::Text(text) => {
// Try to fit text on the current page and if they do not fit,
// return the appropriate OutOfBounds message
let fit = self.layout.layout_text(text.as_ref(), 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.layout_height(init_cursor, *cursor),
};
}
}
}
}
}
LayoutFit::Fitting {
processed_chars: total_processed_chars,
height: self.layout.layout_height(init_cursor, *cursor),
}
}
/// Gets rid of all action-Ops that are before the `skip_bytes` threshold.
/// (Not removing the style changes, e.g. Font or Color, because they need
/// to be correctly set for future Text operations.)
fn filter_skipped_ops<'b, I>(ops_iter: I, skip_bytes: usize) -> impl Iterator<Item = Op<T>> + 'b
where
I: Iterator<Item = &'b Op<T>> + 'b,
'a: 'b,
{
let mut skipped = 0;
ops_iter.filter_map(move |op| {
match op {
Op::Text(text) if skipped < skip_bytes => {
let skip_text_bytes_if_fits_partially = skip_bytes - skipped;
skipped = skipped.saturating_add(text.as_ref().len());
if skipped > skip_bytes {
// Fits partially
// Skipping some bytes at the beginning, leaving rest
Some(Op::Text(
text.skip_prefix(skip_text_bytes_if_fits_partially),
))
} else {
// Does not fit at all
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()),
}
})
}
}
// Op-adding operations
impl<T: StringType + Clone> OpTextLayout<T> {
pub fn with_new_item(mut self, item: Op<T>) -> Self {
self.ops
.push(item)
.assert_if_debugging_ui("Could not push to self.ops - increase MAX_OPS.");
self
}
pub fn text(self, text: T) -> Self {
self.with_new_item(Op::Text(text))
}
pub fn newline(self) -> Self {
self.with_new_item(Op::Text("\n".into()))
}
pub fn newline_half(self) -> Self {
self.with_new_item(Op::Text("\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))
}
pub fn line_breaking(self, line_breaking: LineBreaking) -> Self {
self.with_new_item(Op::LineBreaking(line_breaking))
}
}
// Op-adding aggregation operations
impl<T: StringType + Clone> OpTextLayout<T> {
pub fn text_normal(self, text: T) -> Self {
self.font(Font::NORMAL).text(text)
}
pub fn text_mono(self, text: T) -> Self {
self.font(Font::MONO).text(text)
}
pub fn text_bold(self, text: T) -> Self {
self.font(Font::BOLD).text(text)
}
}
#[derive(Clone)]
pub enum Op<T: StringType> {
/// Render text with current color and font.
Text(T),
/// Set current text color.
Color(Color),
/// Set currently used font.
Font(Font),
/// Set currently used line alignment.
Alignment(Alignment),
/// Set currently used line breaking algorithm.
LineBreaking(LineBreaking),
/// Move the current cursor by specified Offset.
CursorOffset(Offset),
/// Force continuing on the next page.
NextPage,
}

@ -1,9 +1,12 @@
use heapless::Vec;
use crate::ui::{
component::{Component, Event, EventCtx, Never, Paginate},
display::toif::Icon,
geometry::{Alignment, Insets, LinearPlacement, Offset, Point, Rect, TOP_LEFT},
use crate::{
strutil::StringType,
ui::{
component::{Component, Event, EventCtx, Never, Paginate},
display::toif::Icon,
geometry::{Alignment, Insets, LinearPlacement, Offset, Point, Rect, TOP_LEFT},
},
};
use super::layout::{LayoutFit, TextLayout, TextStyle};
@ -24,28 +27,9 @@ pub const PARAGRAPH_BOTTOM_SPACE: i16 = 5;
pub type ParagraphVecLong<T> = Vec<Paragraph<T>, 32>;
pub type ParagraphVecShort<T> = Vec<Paragraph<T>, 8>;
/// Trait for internal representation of strings, which need to support
/// converting to short-lived &str reference as well as creating a new string by
/// skipping some number of bytes. Exists so that we can support `StrBuffer` as
/// well as `&'static str`.
///
/// NOTE: do not implement this trait for `&'static str` in firmware. We always
/// use StrBuffer because using multiple internal representations results in
/// multiple copies of the code in flash memory.
pub trait ParagraphStrType: AsRef<str> {
fn skip_prefix(&self, bytes: usize) -> Self;
}
#[cfg(feature = "bootloader")]
impl ParagraphStrType for &str {
fn skip_prefix(&self, chars: usize) -> Self {
&self[chars..]
}
}
pub trait ParagraphSource {
/// Determines the output type produced.
type StrType: ParagraphStrType;
type StrType: StringType;
/// Return text and associated style for given paragraph index and character
/// offset within the paragraph.
@ -115,7 +99,7 @@ where
/// Helper for `change_offset` which should not get monomorphized as it
/// doesn't refer to T or Self.
fn dyn_change_offset<S: ParagraphStrType>(
fn dyn_change_offset<S: StringType>(
mut area: Rect,
mut offset: PageOffset,
source: &dyn ParagraphSource<StrType = S>,
@ -149,7 +133,7 @@ where
/// Iterate over visible layouts (bounding box, style) together
/// with corresponding string content. Should not get monomorphized.
fn foreach_visible<'a, S: ParagraphStrType>(
fn foreach_visible<'a, S: StringType>(
source: &'a dyn ParagraphSource<StrType = S>,
visible: &'a [TextLayout],
offset: PageOffset,
@ -366,7 +350,7 @@ impl PageOffset {
///
/// If the returned remaining area is not None then it holds that
/// `next_offset.par == self.par + 1`.
fn advance<S: ParagraphStrType>(
fn advance<S: StringType>(
mut self,
area: Rect,
source: &dyn ParagraphSource<StrType = S>,
@ -432,7 +416,7 @@ impl PageOffset {
)
}
fn should_place_pair_on_next_page<S: ParagraphStrType>(
fn should_place_pair_on_next_page<S: StringType>(
this_paragraph: &Paragraph<S>,
next_paragraph: &Paragraph<S>,
area: Rect,
@ -483,7 +467,7 @@ struct PageBreakIterator<'a, T> {
}
impl<T: ParagraphSource> PageBreakIterator<'_, T> {
fn dyn_next<S: ParagraphStrType>(
fn dyn_next<S: StringType>(
mut area: Rect,
paragraphs: &dyn ParagraphSource<StrType = S>,
mut offset: PageOffset,
@ -629,6 +613,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) {
@ -658,7 +653,7 @@ where
}
}
impl<T: ParagraphStrType, const N: usize> ParagraphSource for Vec<Paragraph<T>, N> {
impl<T: StringType, const N: usize> ParagraphSource for Vec<Paragraph<T>, N> {
type StrType = T;
fn at(&self, index: usize, offset: usize) -> Paragraph<Self::StrType> {
@ -671,7 +666,7 @@ impl<T: ParagraphStrType, const N: usize> ParagraphSource for Vec<Paragraph<T>,
}
}
impl<T: ParagraphStrType, const N: usize> ParagraphSource for [Paragraph<T>; N] {
impl<T: StringType, const N: usize> ParagraphSource for [Paragraph<T>; N] {
type StrType = T;
fn at(&self, index: usize, offset: usize) -> Paragraph<Self::StrType> {
@ -684,7 +679,7 @@ impl<T: ParagraphStrType, const N: usize> ParagraphSource for [Paragraph<T>; N]
}
}
impl<T: ParagraphStrType> ParagraphSource for Paragraph<T> {
impl<T: StringType> ParagraphSource for Paragraph<T> {
type StrType = T;
fn at(&self, index: usize, offset: usize) -> Paragraph<Self::StrType> {

@ -0,0 +1,35 @@
use crate::ui::{
display::{Color, Font},
geometry::{Alignment, Rect},
};
use super::{
layout::{LayoutFit, TextLayout},
TextStyle,
};
/// 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 layout_fit = text_layout.render_text(text);
match layout_fit {
LayoutFit::Fitting { height, .. } => Some(area.split_top(height).1),
LayoutFit::OutOfBounds { .. } => None,
}
}

@ -11,10 +11,6 @@ pub struct Timeout {
timer: Option<TimerToken>,
}
pub enum TimeoutMsg {
TimedOut,
}
impl Timeout {
pub fn new(time_ms: u32) -> Self {
Self {
@ -25,7 +21,7 @@ impl Timeout {
}
impl Component for Timeout {
type Msg = TimeoutMsg;
type Msg = ();
fn place(&mut self, _bounds: Rect) -> Rect {
Rect::zero()
@ -41,7 +37,7 @@ impl Component for Timeout {
// Fire.
Event::Timer(token) if Some(token) == self.timer => {
self.timer = None;
Some(TimeoutMsg::TimedOut)
Some(())
}
_ => None,
}

@ -9,11 +9,19 @@ use core::slice;
use super::{get_color_table, get_offset, pixeldata, set_window, Color};
/// Representation of a single glyph.
/// We use standard typographic terms. For a nice explanation, see, e.g.,
/// the FreeType docs at https://www.freetype.org/freetype2/docs/glyphs/glyphs-3.html
pub struct Glyph {
/// Total width of the glyph itself
pub width: i16,
/// Total height of the glyph itself
pub height: i16,
/// Advance - how much to move the cursor after drawing this glyph
pub adv: i16,
/// Left-side horizontal bearing
pub bearing_x: i16,
/// Top-side vertical bearing
pub bearing_y: i16,
data: &'static [u8],
}
@ -52,6 +60,12 @@ impl Glyph {
}
}
/// Space between the right edge of the glyph and the left edge of the next
/// bounding box.
pub const fn right_side_bearing(&self) -> i16 {
self.adv - self.width - self.bearing_x
}
pub fn print(&self, pos: Point, colortable: [Color; 16]) -> i16 {
let bearing = Offset::new(self.bearing_x, -self.bearing_y);
let size = Offset::new(self.width, self.height);
@ -129,6 +143,36 @@ 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 {
if text.is_empty() {
// No text, no width.
return 0;
}
let first_char = unwrap!(text.chars().next());
let first_char_glyph = unwrap!(self.get_glyph(first_char as u8));
let last_char = unwrap!(text.chars().last());
let last_char_glyph = unwrap!(self.get_glyph(last_char as u8));
// Strip leftmost and rightmost spaces/bearings/margins.
self.text_width(text) - first_char_glyph.bearing_x - last_char_glyph.right_side_bearing()
}
/// 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())
}

@ -126,16 +126,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 +147,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 +176,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 +769,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,
@ -855,7 +869,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 +883,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,

@ -25,8 +25,12 @@ 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());
pub fn render_icon(icon: &Icon, center: Point, fg_color: Color, bg_color: Color) {
render_toif(&icon.toif, center, fg_color, bg_color);
}
pub fn render_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 {
@ -177,6 +181,13 @@ impl<'i> Toif<'i> {
}
}
pub const fn is_grayscale(&self) -> bool {
matches!(
self.format(),
ToifFormat::GrayScaleOH | ToifFormat::GrayScaleEH
)
}
pub const fn width(&self) -> i16 {
u16::from_le_bytes([self.data[4], self.data[5]]) as i16
}
@ -204,11 +215,20 @@ impl<'i> Toif<'i> {
) -> UzlibContext {
UzlibContext::new(self.zdata(), window)
}
/// Display the data with baseline Point, aligned according to the
/// `alignment` argument.
pub fn draw(&self, baseline: Point, alignment: Alignment2D, fg_color: Color, bg_color: Color) {
let r = Rect::snap(baseline, self.size(), alignment);
render_toif(self, r.center(), fg_color, bg_color);
}
}
#[derive(PartialEq, Eq, Clone, Copy)]
pub struct Icon {
pub toif: Toif<'static>,
#[cfg(feature = "ui_debug")]
pub name: &'static str,
}
impl Icon {
@ -218,13 +238,27 @@ impl Icon {
None => panic!("Invalid image."),
};
assert!(matches!(toif.format(), ToifFormat::GrayScaleEH));
Self { toif }
Self {
toif,
#[cfg(feature = "ui_debug")]
name: "<unnamed>",
}
}
/// Create a named icon.
/// The name is only stored in debug builds.
pub const fn debug_named(data: &'static [u8], name: &'static str) -> Self {
Self {
#[cfg(feature = "ui_debug")]
name,
..Self::new(data)
}
}
/// Display the icon with baseline Point, aligned according to the
/// `alignment` argument.
pub fn draw(&self, baseline: Point, alignment: Alignment2D, fg_color: Color, bg_color: Color) {
let r = Rect::snap(baseline, self.toif.size(), alignment);
icon(self, r.center(), fg_color, bg_color);
render_icon(self, r.center(), fg_color, bg_color);
}
}

@ -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,17 @@ 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. In case left and right cannot be the same size, right is 1px
/// wider.
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),

@ -8,9 +8,11 @@ use crate::{
obj::Obj,
util::try_or_raise,
},
storage::{get_avatar_len, load_avatar},
strutil::SkipPrefix,
ui::{
component::text::{
paragraphs::{Paragraph, ParagraphSource, ParagraphStrType},
paragraphs::{Paragraph, ParagraphSource},
TextStyle,
},
util::set_animation_disabled,
@ -20,37 +22,32 @@ 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},
};
use crate::ui::display::tjpgd::{jpeg_info, jpeg_test};
use crate::{micropython::buffer::get_buffer, 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"));
let mut vec = Vec::<Obj, N>::new();
let mut iter_buf = IterBuf::new();
for item in Iter::try_from_obj_with_buf(iterable, &mut iter_buf)? {
vec.push(item).map_err(|_| err)?;
}
pub fn iter_into_array<T, E, const N: usize>(iterable: Obj) -> Result<[T; N], Error>
where
T: TryFrom<Obj, Error = E>,
Error: From<E>,
{
let vec: Vec<T, N> = iter_into_vec(iterable)?;
// Returns error if array.len() != N
vec.into_array().map_err(|_| err)
vec.into_array()
.map_err(|_| value_error!("Invalid iterable length"))
}
pub fn iter_into_array<T, const N: usize>(iterable: Obj) -> Result<[T; N], Error>
pub fn iter_into_vec<T, E, const N: usize>(iterable: Obj) -> Result<Vec<T, N>, Error>
where
T: TryFrom<Obj, Error = Error>,
T: TryFrom<Obj, Error = E>,
Error: From<E>,
{
let err = Error::ValueError(cstr!("Invalid iterable length"));
let mut vec = Vec::<T, N>::new();
let mut iter_buf = IterBuf::new();
for item in Iter::try_from_obj_with_buf(iterable, &mut iter_buf)? {
vec.push(item.try_into()?).map_err(|_| err)?;
vec.push(item.try_into()?)
.map_err(|_| value_error!("Invalid iterable length"))?;
}
// 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
@ -143,7 +140,7 @@ impl ParagraphSource for PropsList {
fn at(&self, index: usize, offset: usize) -> Paragraph<Self::StrType> {
let block = move || {
let entry = self.items.get(index / 2)?;
let [key, value, value_is_mono]: [Obj; 3] = iter_into_objs(entry)?;
let [key, value, value_is_mono]: [Obj; 3] = iter_into_array(entry)?;
let value_is_mono: bool = bool::try_from(value_is_mono)?;
let obj: Obj;
let style: &TextStyle;
@ -197,12 +194,6 @@ impl ParagraphSource for PropsList {
}
}
impl ParagraphStrType for StrBuffer {
fn skip_prefix(&self, chars: usize) -> Self {
self.offset(chars)
}
}
pub extern "C" fn upy_disable_animation(disable: Obj) -> Obj {
let block = || {
set_animation_disabled(disable.try_into()?);
@ -214,27 +205,32 @@ pub extern "C" fn upy_disable_animation(disable: Obj) -> Obj {
#[cfg(feature = "jpeg")]
pub extern "C" fn upy_jpeg_info(data: Obj) -> Obj {
let block = || {
let buffer = unsafe { get_buffer(data) };
if let Ok(buffer) = buffer {
let info = jpeg_info(buffer);
if let Some(info) = info {
let obj = unsafe {
let values = [
mp_obj_new_int(info.0.x as _),
mp_obj_new_int(info.0.y as _),
mp_obj_new_int(info.1 as _),
];
mp_obj_new_tuple(3, values.as_ptr())
};
Ok(obj)
} else {
Err(Error::ValueError(cstr!("Invalid image format.")))
}
let buffer = unsafe { get_buffer(data) }?;
if let Some(info) = jpeg_info(buffer) {
let w = info.0.x as u16;
let h = info.0.y as u16;
let mcu_h = info.1 as u16;
(w.into(), h.into(), mcu_h.into()).try_into()
} else {
Err(Error::ValueError(cstr!("Buffer error.")))
Err(value_error!("Invalid image format."))
}
};
unsafe { try_or_raise(block) }
}
pub extern "C" fn upy_toif_info(data: Obj) -> Obj {
let block = || {
let buffer = unsafe { get_buffer(data) }?;
if let Some(toif) = Toif::new(buffer) {
let w = toif.width() as u16;
let h = toif.height() as u16;
let is_grayscale = toif.is_grayscale();
(w.into(), h.into(), is_grayscale.into()).try_into()
} else {
Err(value_error!("Invalid image format."))
}
};
@ -244,11 +240,18 @@ pub extern "C" fn upy_jpeg_info(data: Obj) -> Obj {
#[cfg(feature = "jpeg")]
pub extern "C" fn upy_jpeg_test(data: Obj) -> Obj {
let block = || {
let buffer =
unsafe { get_buffer(data) }.map_err(|_| Error::ValueError(cstr!("Buffer error.")))?;
let buffer = unsafe { get_buffer(data) }?;
let result = jpeg_test(buffer);
Ok(result.into())
};
unsafe { try_or_raise(block) }
}
pub fn get_user_custom_image() -> Result<Gc<[u8]>, Error> {
let len = get_avatar_len()?;
let mut data = Gc::<[u8]>::new_slice(len)?;
// SAFETY: buffer is freshly allocated so nobody else has it.
load_avatar(unsafe { Gc::<[u8]>::as_mut(&mut data) })?;
Ok(data)
}

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

@ -9,6 +9,7 @@ pub mod event;
pub mod geometry;
pub mod lerp;
pub mod screens;
#[macro_use]
pub mod util;
#[cfg(feature = "micropython")]

@ -6,15 +6,17 @@ use crate::ui::{
constant::screen,
display::{Color, Icon},
geometry::{Point, Rect, CENTER},
model_tr::{
component::{Button, ButtonMsg::Clicked},
};
use super::{
super::{
component::Button,
constant::{HEIGHT, WIDTH},
theme::WHITE,
},
ReturnToC,
};
use super::ReturnToC;
#[derive(Copy, Clone)]
pub enum ConfirmMsg {
Cancel = 1,
@ -76,21 +78,22 @@ impl Component for Confirm {
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
if let Some(Clicked) = self.left.event(ctx, event) {
return if self.confirm_left {
Some(Self::Msg::Confirm)
} else {
Some(Self::Msg::Cancel)
};
};
if let Some(Clicked) = self.right.event(ctx, event) {
return if self.confirm_left {
Some(Self::Msg::Cancel)
} else {
Some(Self::Msg::Confirm)
};
};
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
// TODO: to be fixed in bootloader branch
// if let Some(Clicked) = self.left.event(ctx, event) {
// return if self.confirm_left {
// Some(Self::Msg::Confirm)
// } else {
// Some(Self::Msg::Cancel)
// };
// };
// if let Some(Clicked) = self.right.event(ctx, event) {
// return if self.confirm_left {
// Some(Self::Msg::Cancel)
// } else {
// Some(Self::Msg::Confirm)
// };
// };
None
}
@ -111,6 +114,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);

@ -4,18 +4,14 @@ use crate::ui::{
Child, Component, Event, EventCtx, Pad,
},
geometry::{LinearPlacement, Point, Rect},
model_tr::{
bootloader::{
theme::{BLD_BG, TEXT_NORMAL},
title::Title,
ReturnToC,
},
component::ButtonMsg::Clicked,
},
};
use crate::ui::model_tr::{
bootloader::theme::bld_button_default,
use super::super::{
bootloader::{
theme::{bld_button_default, BLD_BG, TEXT_NORMAL},
title::Title,
ReturnToC,
},
component::{Button, ButtonPos},
constant::{HEIGHT, WIDTH},
};
@ -89,13 +85,14 @@ impl Component for Intro {
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
if let Some(Clicked) = self.menu.event(ctx, event) {
return Some(Self::Msg::Menu);
};
if let Some(Clicked) = self.host.event(ctx, event) {
return Some(Self::Msg::Host);
};
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
// TODO: to be fixed in bootloader branch
// if let Some(Clicked) = self.menu.event(ctx, event) {
// return Some(Self::Msg::Menu);
// };
// if let Some(Clicked) = self.host.event(ctx, event) {
// return Some(Self::Msg::Host);
// };
None
}
@ -107,6 +104,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,24 +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},
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
}
}
use super::super::{
bootloader::{theme::BLD_BG, title::Title},
constant::{HEIGHT, WIDTH},
};
pub struct Menu {
bg: Pad,
@ -37,7 +25,7 @@ impl Menu {
}
impl Component for Menu {
type Msg = MenuMsg;
type Msg = Never;
fn place(&mut self, bounds: Rect) -> Rect {
self.bg

@ -4,7 +4,6 @@ use crate::{
component::{Component, Never},
display::{self, Font},
geometry::Point,
model_tr::constant,
},
};
@ -20,22 +19,23 @@ use crate::ui::{
Event, EventCtx,
},
constant::{screen, WIDTH},
display::{fade_backlight_duration, Color, Icon, TextOverlay},
display::{Color, Icon, TextOverlay},
event::ButtonEvent,
geometry::{LinearPlacement, Offset, Rect, CENTER},
model_tr::{
bootloader::{
confirm::Confirm,
intro::Intro,
menu::Menu,
theme::{bld_button_cancel, bld_button_default, BLD_BG, BLD_FG},
},
component::{Button, ButtonPos, ResultScreen},
theme::{BACKLIGHT_NORMAL, ICON_FAIL, ICON_SUCCESS, LOGO_EMPTY},
},
util::{from_c_array, from_c_str},
};
use super::{
component::{Button, ButtonPos, ResultScreen},
constant,
theme::{ICON_FAIL, ICON_SUCCESS, LOGO_EMPTY},
};
use confirm::Confirm;
use intro::Intro;
use menu::Menu;
use theme::{bld_button_cancel, bld_button_default, BLD_BG, BLD_FG};
const SCREEN_ADJ: Rect = screen().split_top(64).0;
pub trait ReturnToC {
@ -78,7 +78,6 @@ where
{
frame.place(SCREEN_ADJ);
frame.paint();
fade_backlight_duration(BACKLIGHT_NORMAL as _, 500);
while button_eval().is_some() {}
@ -139,6 +138,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 +172,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 +221,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(
@ -278,14 +284,7 @@ extern "C" fn screen_wipe_success() {
let m_bottom =
Paragraphs::new(messages).with_placement(LinearPlacement::vertical().align_at_center());
let mut frame = ResultScreen::new(
BLD_FG,
BLD_BG,
Icon::new(ICON_SUCCESS),
m_top,
m_bottom,
true,
);
let mut frame = ResultScreen::new(BLD_FG, BLD_BG, ICON_SUCCESS, m_top, m_bottom, true);
show(&mut frame);
}
@ -305,7 +304,7 @@ extern "C" fn screen_wipe_fail() {
let m_bottom =
Paragraphs::new(messages).with_placement(LinearPlacement::vertical().align_at_center());
let mut frame = ResultScreen::new(BLD_FG, BLD_BG, Icon::new(ICON_FAIL), m_top, m_bottom, true);
let mut frame = ResultScreen::new(BLD_FG, BLD_BG, ICON_FAIL, m_top, m_bottom, true);
show(&mut frame);
}
@ -329,7 +328,7 @@ extern "C" fn screen_install_fail() {
let m_bottom =
Paragraphs::new(messages).with_placement(LinearPlacement::vertical().align_at_center());
let mut frame = ResultScreen::new(BLD_FG, BLD_BG, Icon::new(ICON_FAIL), m_top, m_bottom, true);
let mut frame = ResultScreen::new(BLD_FG, BLD_BG, ICON_FAIL, m_top, m_bottom, true);
show(&mut frame);
}
@ -347,14 +346,7 @@ fn screen_install_success_bld(msg: &'static str, complete_draw: bool) {
let m_bottom =
Paragraphs::new(messages).with_placement(LinearPlacement::vertical().align_at_center());
let mut frame = ResultScreen::new(
BLD_FG,
BLD_BG,
Icon::new(ICON_SUCCESS),
m_top,
m_bottom,
complete_draw,
);
let mut frame = ResultScreen::new(BLD_FG, BLD_BG, ICON_SUCCESS, m_top, m_bottom, complete_draw);
show(&mut frame);
}
@ -372,14 +364,7 @@ fn screen_install_success_initial(msg: &'static str, complete_draw: bool) {
let m_bottom =
Paragraphs::new(messages).with_placement(LinearPlacement::vertical().align_at_center());
let mut frame = ResultScreen::new(
BLD_FG,
BLD_BG,
Icon::new(ICON_SUCCESS),
m_top,
m_bottom,
complete_draw,
);
let mut frame = ResultScreen::new(BLD_FG, BLD_BG, ICON_SUCCESS, m_top, m_bottom, complete_draw);
show(&mut frame);
}

@ -1,49 +1,23 @@
use crate::ui::{
component::text::TextStyle,
display::{Color, Font},
model_tr::{
component::{ButtonStyle, ButtonStyleSheet},
theme::{BG, BLACK, FG, WHITE},
},
geometry::Offset,
};
use super::super::{
component::ButtonStyleSheet,
theme::{BG, BLACK, FG, WHITE},
};
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, Offset::zero())
}
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, Offset::zero())
}
pub const TEXT_NORMAL: TextStyle = TextStyle::new(Font::NORMAL, BLD_FG, BLD_BG, BLD_FG, BLD_FG);

@ -2,9 +2,10 @@ use crate::ui::{
component::{Component, Event, EventCtx, Never},
display::{self, Font},
geometry::{Point, Rect},
model_tr::bootloader::theme::{BLD_BG, BLD_FG},
};
use super::theme::{BLD_BG, BLD_FG};
pub struct Title {
version: &'static str,
area: Rect,
@ -48,5 +49,6 @@ impl Component for Title {
);
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, _sink: &mut dyn FnMut(Rect)) {}
}

@ -0,0 +1,280 @@
use heapless::Vec;
use crate::{
error::Error,
strutil::StringType,
ui::{
component::{
text::paragraphs::{Paragraph, ParagraphSource, ParagraphVecShort, Paragraphs, VecExt},
Child, Component, Event, EventCtx, Pad, Paginate, Qr,
},
geometry::Rect,
},
};
use super::{
theme, ButtonController, ButtonControllerMsg, ButtonDetails, ButtonLayout, ButtonPos, Frame,
};
const MAX_XPUBS: usize = 16;
const QR_BORDER: i16 = 3;
pub struct AddressDetails<T>
where
T: StringType,
{
qr_code: Qr,
details_view: Paragraphs<ParagraphVecShort<T>>,
xpub_view: Frame<Paragraphs<Paragraph<T>>, T>,
xpubs: Vec<(T, T), MAX_XPUBS>,
current_page: usize,
current_subpage: usize,
area: Rect,
pad: Pad,
buttons: Child<ButtonController<T>>,
}
impl<T> AddressDetails<T>
where
T: StringType + Clone,
{
pub fn new(
qr_address: T,
case_sensitive: bool,
account: Option<T>,
path: Option<T>,
) -> 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: T, xpub: T) -> 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.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<T> {
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<T> Component for AddressDetails<T>
where
T: StringType + Clone,
{
type Msg = ();
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(());
}
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<T> crate::trace::Trace for AddressDetails<T>
where
T: StringType + Clone,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("AddressDetails");
match self.current_page {
0 => t.child("qr_code", &self.qr_code),
1 => t.child("details_view", &self.details_view),
_ => t.child("xpub_view", &self.xpub_view),
}
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,445 @@
use super::{
theme, Button, ButtonDetails, ButtonLayout, ButtonPos, HoldToConfirm, HoldToConfirmMsg,
};
use crate::{
strutil::StringType,
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, HTCNeedsRelease
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: StringType,
{
Button(Button<T>),
HoldToConfirm(HoldToConfirm<T>),
Nothing,
}
impl<T> ButtonType<T>
where
T: StringType,
{
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(HoldToConfirm::from_button_details(pos, btn_details))
} else {
Self::Button(Button::from_button_details(pos, btn_details))
}
} else {
Self::Nothing
}
}
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: StringType,
{
pos: ButtonPos,
button_type: ButtonType<T>,
}
impl<T> ButtonContainer<T>
where
T: StringType,
{
/// 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);
}
}
/// Trigger an action or end hold.
/// Called when the button is released. If it is a simple button, it returns
/// a Triggered message. If it is a hold-to-confirm button, it ends the
/// hold.
pub fn maybe_trigger(&mut self, ctx: &mut EventCtx) -> Option<ButtonControllerMsg> {
match self.button_type {
ButtonType::Button(_) => Some(ButtonControllerMsg::Triggered(self.pos)),
_ => {
self.hold_ended(ctx);
None
}
}
}
/// 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: StringType,
{
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: StringType,
{
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: StringType,
{
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, self.left_btn.maybe_trigger(ctx))
}
// * ▲
PhysicalButton::Right => {
// _ _
(ButtonState::Nothing, self.right_btn.maybe_trigger(ctx))
}
},
// * ▼ | ▼ *
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, self.middle_btn.maybe_trigger(ctx))
}
_ => (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")]
impl<T: StringType> crate::trace::Trace for ButtonContainer<T> {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
if let ButtonType::Button(btn) = &self.button_type {
btn.trace(t);
} else if let ButtonType::HoldToConfirm(htc) = &self.button_type {
htc.trace(t);
}
}
}
#[cfg(feature = "ui_debug")]
impl<T: StringType> crate::trace::Trace for ButtonController<T> {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("ButtonController");
t.child("left_btn", &self.left_btn);
t.child("middle_btn", &self.middle_btn);
t.child("right_btn", &self.right_btn);
}
}

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

@ -0,0 +1,96 @@
use crate::{
strutil::StringType,
ui::{
component::{
base::Never, text::util::text_multiline_split_words, Component, Event, EventCtx,
},
display::Font,
geometry::{Alignment, Rect},
},
};
use super::theme;
const HEADER: &str = "COINJOIN IN PROGRESS";
const FOOTER: &str = "Don't disconnect your Trezor";
pub struct CoinJoinProgress<T> {
text: T,
area: Rect,
}
impl<T> CoinJoinProgress<T>
where
T: StringType,
{
pub fn new(text: T, _indeterminate: bool) -> Self {
Self {
text,
area: Rect::zero(),
}
}
}
impl<T> Component for CoinJoinProgress<T>
where
T: StringType,
{
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::BOLD,
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<T> crate::trace::Trace for CoinJoinProgress<T>
where
T: StringType,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("CoinJoinProgress");
t.string("header", HEADER);
t.string("text", self.text.as_ref());
t.string("footer", FOOTER);
}
}

@ -0,0 +1,28 @@
use crate::ui::{
display::{self, Font},
geometry::Point,
};
use super::theme;
/// Display white text on black background
pub fn display<T: AsRef<str>>(baseline: Point, text: &T, font: Font) {
display::text_left(baseline, text.as_ref(), font, theme::FG, theme::BG);
}
/// Display black text on white background
pub fn display_inverse<T: AsRef<str>>(baseline: Point, text: &T, font: Font) {
display::text_left(baseline, text.as_ref(), font, theme::BG, theme::FG);
}
/// Display white text on black background,
/// centered around a baseline Point
pub fn display_center<T: AsRef<str>>(baseline: Point, text: &T, font: Font) {
display::text_center(baseline, text.as_ref(), font, theme::FG, theme::BG);
}
/// Display white text on black background,
/// with right boundary at a baseline Point
pub fn display_right<T: AsRef<str>>(baseline: Point, text: &T, font: Font) {
display::text_right(baseline, text.as_ref(), font, theme::FG, theme::BG);
}

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

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

@ -0,0 +1,302 @@
use crate::{
strutil::StringType,
ui::{
component::{Child, Component, ComponentExt, Event, EventCtx, Pad, Paginate},
geometry::Rect,
},
};
use super::{
scrollbar::SCROLLBAR_SPACE, theme, title::Title, ButtonAction, ButtonController,
ButtonControllerMsg, ButtonLayout, ButtonPos, CancelInfoConfirmMsg, FlowPages, Page, ScrollBar,
};
pub struct Flow<F, T>
where
F: Fn(usize) -> Page<T>,
T: StringType + Clone,
{
/// Function to get pages from
pages: FlowPages<F, T>,
/// Instance of the current Page
current_page: Page<T>,
/// Title being shown at the top in bold
title: Option<Title<T>>,
scrollbar: Child<ScrollBar>,
content_area: Rect,
title_area: Rect,
pad: Pad,
buttons: Child<ButtonController<T>>,
page_counter: usize,
return_confirmed_index: bool,
}
impl<F, T> Flow<F, T>
where
F: Fn(usize) -> Page<T>,
T: StringType + Clone,
{
pub fn new(pages: FlowPages<F, T>) -> 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: T) -> 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
}
pub fn confirmed_index(&self) -> Option<usize> {
self.return_confirmed_index.then_some(self.page_counter)
}
/// 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.change_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 the first page.
fn go_to_first_page(&mut self, ctx: &mut EventCtx) {
self.page_counter = 0;
self.update(ctx, true);
}
/// Going to the first page.
fn go_to_last_page(&mut self, ctx: &mut EventCtx) {
self.page_counter = self.pages.count() - 1;
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);
});
}
/// Current choice is still the same, only its inner state has changed
/// (its sub-page changed).
fn update_after_current_choice_inner_change(&mut self, ctx: &mut EventCtx) {
let inner_page = self.current_page.get_current_page();
self.scrollbar.mutate(ctx, |ctx, scrollbar| {
scrollbar.change_page(self.page_counter + inner_page);
scrollbar.request_complete_repaint(ctx);
});
self.update(ctx, false);
}
/// When current choice contains paginated content, it may use the button
/// event to just paginate itself.
fn event_consumed_by_current_choice(&mut self, ctx: &mut EventCtx, pos: ButtonPos) -> bool {
if matches!(pos, ButtonPos::Left) && self.current_page.has_prev_page() {
self.current_page.go_to_prev_page();
self.update_after_current_choice_inner_change(ctx);
true
} else if matches!(pos, ButtonPos::Right) && self.current_page.has_next_page() {
self.current_page.go_to_next_page();
self.update_after_current_choice_inner_change(ctx);
true
} else {
false
}
}
}
impl<F, T> Component for Flow<F, T>
where
F: Fn(usize) -> Page<T>,
T: StringType + Clone,
{
type Msg = CancelInfoConfirmMsg;
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);
// Redefining scrollbar now when we have its page_count
self.scrollbar = Child::new(ScrollBar::new(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::FirstPage => {
self.go_to_first_page(ctx);
return None;
}
ButtonAction::LastPage => {
self.go_to_last_page(ctx);
return None;
}
ButtonAction::Cancel => return Some(CancelInfoConfirmMsg::Cancelled),
ButtonAction::Confirm => return Some(CancelInfoConfirmMsg::Confirmed),
ButtonAction::Info => return Some(CancelInfoConfirmMsg::Info),
}
}
};
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")]
impl<F, T> crate::trace::Trace for Flow<F, T>
where
F: Fn(usize) -> Page<T>,
T: StringType + Clone,
{
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);
if let Some(title) = &self.title {
t.child("title", title);
}
t.child("scrollbar", &self.scrollbar);
t.child("buttons", &self.buttons);
t.child("flow_page", &self.current_page);
}
}

@ -0,0 +1,224 @@
use crate::{
strutil::StringType,
ui::{
component::{base::Component, text::layout::LayoutFit, FormattedText, Paginate},
geometry::Rect,
},
};
use super::{ButtonActions, ButtonDetails, ButtonLayout};
// So that there is only one implementation, and not multiple generic ones
// as would be via `const N: usize` generics.
const MAX_OPS_PER_PAGE: usize = 15;
/// 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, T>
where
T: StringType + Clone,
F: Fn(usize) -> Page<T>,
{
/// Function/closure that will return appropriate page on demand.
get_page: F,
/// Number of pages in the flow.
page_count: usize,
}
impl<F, T> FlowPages<F, T>
where
F: Fn(usize) -> Page<T>,
T: StringType + Clone,
{
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<T> {
(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<T>
where
T: StringType + Clone,
{
formatted: FormattedText<T>,
btn_layout: ButtonLayout<T>,
btn_actions: ButtonActions,
current_page: usize,
page_count: usize,
title: Option<T>,
}
// For `layout.rs`
impl<T> Page<T>
where
T: StringType + Clone,
{
pub fn new(
btn_layout: ButtonLayout<T>,
btn_actions: ButtonActions,
formatted: FormattedText<T>,
) -> Self {
Self {
formatted,
btn_layout,
btn_actions,
current_page: 0,
page_count: 1,
title: None,
}
}
}
// For `flow.rs`
impl<T> Page<T>
where
T: StringType + Clone,
{
/// Adding title.
pub fn with_title(mut self, title: T) -> Self {
self.title = Some(title);
self
}
pub fn paint(&mut self) {
self.change_page(self.current_page);
self.formatted.paint();
}
pub fn place(&mut self, bounds: Rect) -> Rect {
self.formatted.place(bounds);
self.page_count = self.page_count();
bounds
}
pub fn btn_layout(&self) -> ButtonLayout<T> {
// 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<T> {
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
}
}
// Pagination
impl<T> Paginate for Page<T>
where
T: StringType + Clone,
{
fn page_count(&mut self) -> usize {
self.formatted.page_count()
}
fn change_page(&mut self, to_page: usize) {
self.formatted.change_page(to_page)
}
}
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for Page<T>
where
T: StringType + Clone,
{
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.as_ref());
}
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.formatted.layout_content_debug(&mut TraceSink(l));
fit.set(Some(result));
});
}
}

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

@ -0,0 +1,140 @@
use crate::{
strutil::StringType,
time::{Duration, Instant},
ui::{
component::{Component, Event, EventCtx},
event::ButtonEvent,
geometry::Rect,
},
};
use super::{
loader::{Loader, DEFAULT_DURATION_MS},
theme, ButtonContent, ButtonDetails, ButtonPos, LoaderMsg, LoaderStyleSheet,
};
pub enum HoldToConfirmMsg {
Confirmed,
FailedToConfirm,
}
pub struct HoldToConfirm<T>
where
T: StringType,
{
pos: ButtonPos,
loader: Loader<T>,
text_width: i16,
}
impl<T> HoldToConfirm<T>
where
T: StringType,
{
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,
}
}
pub fn from_button_details(pos: ButtonPos, btn_details: ButtonDetails<T>) -> Self {
let duration = btn_details
.duration
.unwrap_or_else(|| Duration::from_millis(DEFAULT_DURATION_MS));
match btn_details.content {
ButtonContent::Text(text) => {
Self::text(pos, text, LoaderStyleSheet::default_loader(), duration)
}
ButtonContent::Icon(_) => panic!("Icon is not supported"),
}
}
/// 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: StringType,
{
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: StringType,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("HoldToConfirm");
t.child("loader", &self.loader);
}
}

@ -0,0 +1,229 @@
use crate::{
strutil::StringType,
trezorhal::usb::usb_configured,
ui::{
component::{Child, Component, Event, EventCtx, Label},
display::{rect_fill, toif::Toif, Font},
event::USBEvent,
geometry::{self, Insets, Offset, Point, Rect},
layout::util::get_user_custom_image,
},
};
use super::{
super::constant, 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 - 15;
const LABEL_AREA: Rect = AREA.split_top(LABEL_Y).1;
const LOCKED_INSTRUCTION_Y: i16 = 27;
const LOCKED_INSTRUCTION_AREA: Rect = AREA.split_top(LOCKED_INSTRUCTION_Y).1;
const LOGO_ICON_TOP_MARGIN: i16 = 12;
const LOCK_ICON_TOP_MARGIN: i16 = 12;
const NOTIFICATION_HEIGHT: i16 = 12;
const LABEL_OUTSET: i16 = 3;
pub struct Homescreen<T>
where
T: StringType,
{
// TODO label should be a Child in theory, but the homescreen image is not, so it is
// always painted, so we need to always paint the label too
label: Label<T>,
notification: Option<(T, u8)>,
/// Used for HTC functionality to lock device from homescreen
invisible_buttons: Child<ButtonController<T>>,
}
impl<T> Homescreen<T>
where
T: StringType + Clone,
{
pub fn new(label: T, notification: Option<(T, u8)>) -> Self {
let invisible_btn_layout = ButtonLayout::htc_none_htc("".into(), "".into());
Self {
label: Label::centered(label, theme::TEXT_NORMAL),
notification,
invisible_buttons: Child::new(ButtonController::new(invisible_btn_layout)),
}
}
fn paint_homescreen_image(&self) {
if let Ok(user_custom_image) = get_user_custom_image() {
let toif_data = unwrap!(Toif::new(user_custom_image.as_ref()));
toif_data.draw(TOP_CENTER, geometry::TOP_CENTER, theme::FG, theme::BG);
} else {
theme::ICON_LOGO.draw(
TOP_CENTER + Offset::y(LOGO_ICON_TOP_MARGIN),
geometry::TOP_CENTER,
theme::FG,
theme::BG,
);
}
}
fn paint_notification(&self) {
let baseline = TOP_CENTER + Offset::y(Font::MONO.line_height());
if !usb_configured() {
self.fill_notification_background();
// TODO: fill warning icons here as well?
display_center(baseline, &"NO USB CONNECTION", Font::MONO);
} else if let Some((notification, _level)) = &self.notification {
self.fill_notification_background();
// TODO: what if the notification text is so long it collides with icons?
self.paint_warning_icons_in_top_corners();
display_center(baseline, &notification.as_ref(), Font::MONO);
}
}
fn paint_label(&mut self) {
// paint black background to place the label
let mut outset = Insets::uniform(LABEL_OUTSET);
// the margin at top is bigger (caused by text-height vs line-height?)
// compensate by shrinking the outset
outset.top -= 1;
rect_fill(self.label.text_area().outset(outset), theme::BG);
self.label.paint();
}
/// So that notification is well visible even on homescreen image
fn fill_notification_background(&self) {
rect_fill(AREA.split_top(NOTIFICATION_HEIGHT).0, theme::BG);
}
fn paint_warning_icons_in_top_corners(&self) {
let warning_icon = 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 event_usb(&mut self, ctx: &mut EventCtx, event: Event) {
if let Event::USB(USBEvent::Connected(_)) = event {
ctx.request_paint();
}
}
}
impl<T> Component for Homescreen<T>
where
T: StringType + Clone,
{
type Msg = ();
fn place(&mut self, bounds: Rect) -> Rect {
self.label.place(LABEL_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(());
}
None
}
fn paint(&mut self) {
// Painting the homescreen image first, as the notification and label
// should be "on top of it"
self.paint_homescreen_image();
self.paint_notification();
self.paint_label();
}
}
pub struct Lockscreen<T>
where
T: StringType,
{
label: Child<Label<T>>,
instruction: Child<Label<T>>,
/// Used for unlocking the device from lockscreen
invisible_buttons: Child<ButtonController<T>>,
}
impl<T> Lockscreen<T>
where
T: StringType + Clone,
{
pub fn new(label: T, bootscreen: bool) -> Self {
let invisible_btn_layout = ButtonLayout::text_none_text("".into(), "".into());
let instruction_str = if bootscreen {
"Click to Connect"
} else {
"Click to Unlock"
};
Lockscreen {
label: Child::new(Label::centered(label, theme::TEXT_NORMAL)),
instruction: Child::new(Label::centered(instruction_str.into(), theme::TEXT_MONO)),
invisible_buttons: Child::new(ButtonController::new(invisible_btn_layout)),
}
}
}
impl<T> Component for Lockscreen<T>
where
T: StringType + Clone,
{
type Msg = ();
fn place(&mut self, bounds: Rect) -> Rect {
self.label.place(LABEL_AREA);
self.instruction.place(LOCKED_INSTRUCTION_AREA);
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(());
}
None
}
fn paint(&mut self) {
theme::ICON_LOCK.draw(
TOP_CENTER + Offset::y(LOCK_ICON_TOP_MARGIN),
geometry::TOP_CENTER,
theme::FG,
theme::BG,
);
self.instruction.paint();
self.label.paint();
}
}
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for Homescreen<T>
where
T: StringType,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("Homescreen");
t.child("label", &self.label);
}
}
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for Lockscreen<T>
where
T: StringType,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("Lockscreen");
t.child("label", &self.label);
}
}

@ -0,0 +1,464 @@
use crate::{
strutil::StringType,
ui::{
component::{Child, Component, Event, EventCtx, Pad},
geometry::{Offset, Rect},
},
};
use super::{
super::{theme, ButtonController, ButtonControllerMsg, ButtonLayout, ButtonPos},
choice_item::ChoiceItem,
};
const DEFAULT_ITEMS_DISTANCE: i16 = 10;
const DEFAULT_Y_BASELINE: i16 = 20;
pub trait Choice<T: StringType> {
fn paint_center(&self, area: Rect, inverse: bool);
fn width_center(&self) -> i16;
fn paint_side(&self, area: Rect);
fn width_side(&self) -> i16;
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: StringType> {
type Action;
fn count(&self) -> usize;
fn get(&self, index: usize) -> (ChoiceItem<T>, Self::Action);
}
/// 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, A>
where
F: ChoiceFactory<T, Action = A>,
T: StringType,
{
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, A> ChoicePage<F, T, A>
where
F: ChoiceFactory<T, Action = A>,
T: StringType + Clone,
{
pub fn new(choices: F) -> Self {
let initial_btn_layout = choices.get(0).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.get_current_choice().0.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.get_current_choice().0.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()
}
/// Getting the choice on the current index
pub fn get_current_choice(&self) -> (ChoiceItem<T>, A) {
self.choices.get(self.page_counter)
}
/// Display the current choice in the middle.
fn show_current_choice(&mut self, area: Rect) {
self.get_current_choice()
.0
.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) {
// NOTE: 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 current_area = area.split_right(self.items_distance).0;
while current_area.width() > 0 {
// 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 (choice, _) = self.choices.get(page_index as usize);
let choice_width = choice.width_side();
if current_area.width() <= choice_width && !self.show_incomplete {
// early break for an item that will not fit the remaining space
break;
}
// We need to calculate the area explicitly because we want to allow it
// to exceed the bounds of the original area.
let choice_area = Rect::from_top_right_and_size(
current_area.top_right(),
Offset::new(choice_width, current_area.height()),
);
choice.paint_side(choice_area);
// Updating loop variables.
current_area = current_area
.split_right(choice_width + self.items_distance)
.0;
page_index -= 1;
}
}
/// 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;
// start with a little offset to account for the middle highlight
let mut current_area = area.split_left(self.items_distance + 3).1;
while current_area.width() > 0 {
// 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 (choice, _) = self.choices.get(page_index);
let choice_width = choice.width_side();
if current_area.width() <= choice_width && !self.show_incomplete {
// early break for an item that will not fit the remaining space
break;
}
// We need to calculate the area explicitly because we want to allow it
// to exceed the bounds of the original area.
let choice_area = Rect::from_top_left_and_size(
current_area.top_left(),
Offset::new(choice_width, current_area.height()),
);
choice.paint_side(choice_area);
// Updating loop variables.
current_area = current_area
.split_left(choice_width + self.items_distance)
.1;
page_index += 1;
}
}
/// 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.get_current_choice().0.btn_layout();
self.buttons.mutate(ctx, |_ctx, buttons| {
buttons.set(btn_layout);
});
}
pub fn choice_factory(&self) -> &F {
&self.choices
}
}
impl<F, T, A> Component for ChoicePage<F, T, A>
where
F: ChoiceFactory<T, Action = A>,
T: StringType + Clone,
{
type Msg = A;
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);
}
}
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);
}
}
ButtonPos::Middle => {
// Clicked SELECT. Send current choice index
self.clear_and_repaint(ctx);
return Some(self.get_current_choice().1);
}
}
};
// 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, A> crate::trace::Trace for ChoicePage<F, T, A>
where
F: ChoiceFactory<T, Action = A>,
T: StringType + Clone,
{
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).0);
} 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()).0);
}
t.child("current_choice", &self.choices.get(self.page_counter).0);
if self.has_next_choice() {
t.child("next_choice", &self.choices.get(self.page_counter + 1).0);
} else if self.is_carousel {
// In case of carousel going to the very left.
t.child("next_choice", &self.choices.get(0).0);
}
t.child("buttons", &self.buttons);
}
}

@ -0,0 +1,183 @@
use crate::{
strutil::{ShortString, StringType},
ui::{
display::{self, rect_fill, rect_fill_corners, rect_outline_rounded, Font, Icon},
geometry::{Offset, Rect, BOTTOM_LEFT},
},
};
use heapless::String;
use super::super::{theme, ButtonDetails, ButtonLayout, Choice};
const ICON_RIGHT_PADDING: i16 = 2;
/// Simple string component used as a choice item.
#[derive(Clone)]
pub struct ChoiceItem<T: StringType> {
text: ShortString,
icon: Option<Icon>,
btn_layout: ButtonLayout<T>,
font: Font,
}
impl<T: StringType> ChoiceItem<T> {
pub fn new<U: AsRef<str>>(text: U, btn_layout: ButtonLayout<T>) -> Self {
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
}
/// 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: ShortString) {
self.text = text;
}
fn side_text(&self) -> Option<&str> {
if self.icon.is_some() {
None
} else {
Some(self.text.as_ref())
}
}
pub fn content(&self) -> &str {
self.text.as_ref()
}
}
impl<T> Choice<T> for ChoiceItem<T>
where
T: StringType + Clone,
{
/// 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) {
let width = text_icon_width(Some(self.text.as_ref()), self.icon, self.font);
paint_rounded_highlight(area, Offset::new(width, self.font.text_height()), inverse);
paint_text_icon(
area,
width,
Some(self.text.as_ref()),
self.icon,
self.font,
inverse,
);
}
/// 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 {
text_icon_width(Some(self.text.as_ref()), self.icon, self.font)
}
/// Getting the non-central width in pixels.
/// It will show an icon if defined, otherwise the text, not both.
fn width_side(&self) -> i16 {
text_icon_width(self.side_text(), self.icon, self.font)
}
/// Painting smaller version of the item on the side.
fn paint_side(&self, area: Rect) {
let width = text_icon_width(self.side_text(), self.icon, self.font);
paint_text_icon(area, width, self.side_text(), self.icon, self.font, false);
}
/// Getting current button layout.
fn btn_layout(&self) -> ButtonLayout<T> {
self.btn_layout.clone()
}
}
fn paint_rounded_highlight(area: Rect, size: Offset, inverse: bool) {
let bound = theme::BUTTON_OUTLINE;
let left_bottom = area.bottom_center() + Offset::new(-size.x / 2 - bound, bound + 1);
let x_size = size.x + 2 * bound;
let y_size = size.y + 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);
}
}
fn text_icon_width(text: Option<&str>, icon: Option<Icon>, font: Font) -> i16 {
match (text, icon) {
(Some(text), Some(icon)) => {
icon.toif.width() + ICON_RIGHT_PADDING + font.visible_text_width(text)
}
(Some(text), None) => font.visible_text_width(text),
(None, Some(icon)) => icon.toif.width(),
(None, None) => 0,
}
}
fn paint_text_icon(
area: Rect,
width: i16,
text: Option<&str>,
icon: Option<Icon>,
font: Font,
inverse: bool,
) {
let fg_color = if inverse { theme::BG } else { theme::FG };
let bg_color = if inverse { theme::FG } else { theme::BG };
let mut baseline = area.bottom_center() - Offset::x(width / 2);
if let Some(icon) = icon {
let height_diff = font.text_height() - icon.toif.height();
let vertical_offset = Offset::y(-height_diff / 2);
icon.draw(baseline + vertical_offset, BOTTOM_LEFT, fg_color, bg_color);
baseline = baseline + Offset::x(icon.toif.width() + ICON_RIGHT_PADDING);
}
if let Some(text) = text {
// Possibly shifting the baseline left, when there is a text bearing.
// This is to center the text properly.
baseline = baseline - Offset::x(font.start_x_bearing(text));
display::text_left(baseline, text, font, fg_color, bg_color);
}
}
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
impl<T: StringType> crate::trace::Trace for ChoiceItem<T> {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("ChoiceItem");
t.string("content", self.text.as_ref());
}
}

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

Loading…
Cancel
Save