feat(all): add UI for Model R
63
ci/build.yml
@ -133,6 +133,37 @@ core fw btconly production build:
|
||||
- firmware-T2T1-btconly-production-*.*.*-$CI_COMMIT_SHORT_SHA.bin
|
||||
expire_in: 1 week
|
||||
|
||||
core fw R debug build:
|
||||
stage: build
|
||||
<<: *gitlab_caching
|
||||
needs: []
|
||||
variables:
|
||||
TREZOR_MODEL: "R"
|
||||
PYOPT: "0"
|
||||
script:
|
||||
- nix-shell --run "poetry run make -C core build_firmware"
|
||||
- cp core/build/firmware/firmware.bin trezor-fw-debug-tr-$CORE_VERSION-$CI_COMMIT_SHORT_SHA.bin
|
||||
artifacts:
|
||||
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
|
||||
paths:
|
||||
- trezor-fw-*.*.*-$CI_COMMIT_SHORT_SHA.bin
|
||||
expire_in: 1 week
|
||||
|
||||
core fw R build:
|
||||
stage: build
|
||||
<<: *gitlab_caching
|
||||
needs: []
|
||||
variables:
|
||||
TREZOR_MODEL: "R"
|
||||
script:
|
||||
- nix-shell --run "poetry run make -C core build_firmware"
|
||||
- cp core/build/firmware/firmware.bin trezor-fw-tr-$CORE_VERSION-$CI_COMMIT_SHORT_SHA.bin
|
||||
artifacts:
|
||||
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
|
||||
paths:
|
||||
- trezor-fw-*.*.*-$CI_COMMIT_SHORT_SHA.bin
|
||||
expire_in: 1 week
|
||||
|
||||
# Non-frozen emulator build. This means you still need Python files
|
||||
# present which get interpreted.
|
||||
core unix regular build:
|
||||
@ -235,6 +266,38 @@ core unix frozen debug build:
|
||||
untracked: true
|
||||
expire_in: 1 week
|
||||
|
||||
core unix frozen R debug build:
|
||||
stage: build
|
||||
<<: *gitlab_caching
|
||||
needs: []
|
||||
variables:
|
||||
PYOPT: "0"
|
||||
TREZOR_MODEL: "R"
|
||||
script:
|
||||
- nix-shell --run "poetry run make -C core build_unix_frozen"
|
||||
artifacts:
|
||||
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
|
||||
untracked: true
|
||||
expire_in: 10 weeks
|
||||
|
||||
core unix frozen R debug build arm:
|
||||
image: nixos/nix
|
||||
stage: build
|
||||
<<: *gitlab_caching
|
||||
needs: []
|
||||
variables:
|
||||
PYOPT: "0"
|
||||
TREZOR_MODEL: "R"
|
||||
script:
|
||||
- nix-shell --run "poetry run make -C core build_unix_frozen"
|
||||
- mv core/build/unix/trezor-emu-core core/build/unix/trezor-emu-core-arm
|
||||
artifacts:
|
||||
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
|
||||
untracked: true
|
||||
expire_in: 10 weeks
|
||||
tags:
|
||||
- docker_darwin_arm
|
||||
|
||||
core unix frozen debug asan build:
|
||||
stage: build
|
||||
<<: *gitlab_caching
|
||||
|
@ -238,6 +238,7 @@ ui tests fixtures deploy:
|
||||
- core device test
|
||||
- core persistence test
|
||||
- legacy device test
|
||||
- core device R test
|
||||
script:
|
||||
- echo "Deploying to $DEPLOY_PATH"
|
||||
- rsync --delete -va ci/ui_test_records/* "$DEPLOY_PATH"
|
||||
|
61
ci/test.yml
@ -81,6 +81,37 @@ core device test:
|
||||
reports:
|
||||
junit: tests/junit.xml
|
||||
|
||||
core device R test:
|
||||
stage: test
|
||||
<<: *gitlab_caching
|
||||
needs:
|
||||
- core unix frozen R debug build
|
||||
variables:
|
||||
TREZOR_PROFILING: "1"
|
||||
TREZOR_MODEL: "R"
|
||||
script:
|
||||
- nix-shell --run "poetry run make -C core test_emu_ui | ts -s"
|
||||
after_script:
|
||||
- mv tests/ui_tests/reporting/reports/test/ test_ui_report
|
||||
- nix-shell --run "poetry run python ci/prepare_ui_artifacts.py TR | ts -s"
|
||||
- diff tests/ui_tests/fixtures.json tests/ui_tests/fixtures.suggestion.json
|
||||
- nix-shell --run "cd tests/ui_tests ; poetry run python reporting/report_master_diff.py TR_"
|
||||
- mv tests/ui_tests/reporting/reports/master_diff/ .
|
||||
artifacts:
|
||||
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
|
||||
paths:
|
||||
- ci/ui_test_records/
|
||||
- test_ui_report
|
||||
- tests/ui_tests/screens/
|
||||
- tests/ui_tests/fixtures.suggestion.json
|
||||
- tests/junit.xml
|
||||
- tests/trezor.log
|
||||
- master_diff
|
||||
when: always
|
||||
expire_in: 4 weeks
|
||||
reports:
|
||||
junit: tests/junit.xml
|
||||
|
||||
core device asan test:
|
||||
stage: test
|
||||
<<: *gitlab_caching
|
||||
@ -293,6 +324,36 @@ core click test:
|
||||
expire_in: 1 week
|
||||
when: always
|
||||
|
||||
# Click tests.
|
||||
# See [docs/tests/click-tests](../tests/click-tests.md) for more info.
|
||||
core click R test:
|
||||
stage: test
|
||||
<<: *gitlab_caching
|
||||
needs:
|
||||
- core unix frozen R debug build
|
||||
variables:
|
||||
TREZOR_PROFILING: 1
|
||||
script:
|
||||
- nix-shell --run "poetry run make -C core test_emu_click_ui | ts -s"
|
||||
after_script:
|
||||
- mv core/src/.coverage core/.coverage.test_click
|
||||
- mv tests/ui_tests/reports/test/ test_ui_report
|
||||
- nix-shell --run "poetry run python ci/prepare_ui_artifacts.py | ts -s"
|
||||
- diff -u tests/ui_tests/fixtures.json tests/ui_tests/fixtures.suggestion.json
|
||||
artifacts:
|
||||
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
|
||||
paths:
|
||||
- ci/ui_test_records/
|
||||
- test_ui_report
|
||||
- tests/ui_tests/screens/
|
||||
- tests/ui_tests/fixtures.suggestion.json
|
||||
- tests/trezor.log
|
||||
- tests/junit.xml
|
||||
reports:
|
||||
junit: tests/junit.xml
|
||||
expire_in: 1 week
|
||||
when: always
|
||||
|
||||
core click asan test:
|
||||
stage: test
|
||||
<<: *gitlab_caching
|
||||
|
@ -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];
|
||||
|
1
core/.changelog.d/2610.added
Normal file
@ -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',
|
||||
|
BIN
core/assets/model_r/amount.png
Normal file
After Width: | Height: | Size: 180 B |
BIN
core/assets/model_r/amount_smaller.png
Normal file
After Width: | Height: | Size: 479 B |
BIN
core/assets/model_r/back_up_arrow.png
Normal file
After Width: | Height: | Size: 133 B |
BIN
core/assets/model_r/bin.png
Normal file
After Width: | Height: | Size: 175 B |
BIN
core/assets/model_r/cancel_for_outline.png
Normal file
After Width: | Height: | Size: 164 B |
BIN
core/assets/model_r/cancel_no_outline.png
Normal file
After Width: | Height: | Size: 125 B |
BIN
core/assets/model_r/delete.png
Normal file
After Width: | Height: | Size: 134 B |
BIN
core/assets/model_r/down_arrow.png
Normal file
After Width: | Height: | Size: 162 B |
BIN
core/assets/model_r/eye.png
Normal file
After Width: | Height: | Size: 129 B |
BIN
core/assets/model_r/eye_round.png
Normal file
After Width: | Height: | Size: 139 B |
BIN
core/assets/model_r/homescreen.png
Normal file
After Width: | Height: | Size: 342 B |
BIN
core/assets/model_r/left_arm.png
Normal file
After Width: | Height: | Size: 154 B |
BIN
core/assets/model_r/left_arrow.png
Normal file
After Width: | Height: | Size: 157 B |
BIN
core/assets/model_r/lock.png
Normal file
After Width: | Height: | Size: 170 B |
BIN
core/assets/model_r/logo_22_33.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
BIN
core/assets/model_r/next_page.png
Normal file
After Width: | Height: | Size: 139 B |
BIN
core/assets/model_r/param.png
Normal file
After Width: | Height: | Size: 159 B |
BIN
core/assets/model_r/param_smaller.png
Normal file
After Width: | Height: | Size: 155 B |
BIN
core/assets/model_r/prev_page.png
Normal file
After Width: | Height: | Size: 137 B |
BIN
core/assets/model_r/right_arm.png
Normal file
After Width: | Height: | Size: 165 B |
BIN
core/assets/model_r/right_arrow.png
Normal file
After Width: | Height: | Size: 156 B |
BIN
core/assets/model_r/right_arrow_fat.png
Normal file
After Width: | Height: | Size: 165 B |
BIN
core/assets/model_r/space.png
Normal file
After Width: | Height: | Size: 118 B |
BIN
core/assets/model_r/tick.png
Normal file
After Width: | Height: | Size: 117 B |
BIN
core/assets/model_r/tick_fat.png
Normal file
After Width: | Height: | Size: 164 B |
BIN
core/assets/model_r/up_arrow.png
Normal file
After Width: | Height: | Size: 166 B |
BIN
core/assets/model_r/user.png
Normal file
After Width: | Height: | Size: 176 B |
BIN
core/assets/model_r/user_smaller.png
Normal file
After Width: | Height: | Size: 131 B |
BIN
core/assets/model_r/wallet.png
Normal file
After Width: | Height: | Size: 178 B |
BIN
core/assets/model_r/warning_left.png
Normal file
After Width: | Height: | Size: 178 B |
BIN
core/assets/model_r/warning_right.png
Normal file
After Width: | Height: | Size: 142 B |
@ -6,7 +6,7 @@
|
||||
// - the third, fourth and fifth bytes are advance, bearingX and bearingY of the horizontal metrics of the glyph
|
||||
// - the rest is packed 1-bit glyph data
|
||||
|
||||
/* */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_32[] = { 0, 0, 8, 0, 0 };
|
||||
/* */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_32[] = { 0, 0, 7, 0, 0 }; // width hand-changed from 8 to 7 to have 9px space between words
|
||||
/* ! */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_33[] = { 1, 7, 7, 2, 7, 250 };
|
||||
/* " */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_34[] = { 3, 3, 7, 1, 7, 182, 128 };
|
||||
/* # */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_35[] = { 6, 6, 7, 0, 6, 75, 244, 146, 253, 32 };
|
||||
|
203
core/embed/lib/fonts/font_unifont_bold_16.c
Normal file
@ -0,0 +1,203 @@
|
||||
#include <stdint.h>
|
||||
|
||||
// clang-format off
|
||||
|
||||
// - the first two bytes are width and height of the glyph
|
||||
// - the third, fourth and fifth bytes are advance, bearingX and bearingY of the horizontal metrics of the glyph
|
||||
// - the rest is packed 1-bit glyph data
|
||||
|
||||
/* */ static const uint8_t Font_Unifont_Bold_16_glyph_32[] = { 0, 0, 8, 0, 0 };
|
||||
/* ! */ static const uint8_t Font_Unifont_Bold_16_glyph_33[] = { 2, 10, 7, 2, 10, 255, 252, 240 };
|
||||
/* " */ static const uint8_t Font_Unifont_Bold_16_glyph_34[] = { 6, 4, 7, 0, 12, 207, 60, 209, 0 };
|
||||
/* # */ static const uint8_t Font_Unifont_Bold_16_glyph_35[] = { 7, 10, 8, 0, 10, 54, 108, 223, 246, 205, 191, 236, 217, 176 };
|
||||
/* $ */ static const uint8_t Font_Unifont_Bold_16_glyph_36[] = { 7, 10, 8, 0, 10, 16, 251, 94, 183, 3, 181, 235, 124, 32 };
|
||||
/* % */ static const uint8_t Font_Unifont_Bold_16_glyph_37[] = { 7, 10, 8, 0, 10, 97, 102, 211, 65, 2, 11, 43, 150, 24 };
|
||||
/* & */ static const uint8_t Font_Unifont_Bold_16_glyph_38[] = { 8, 10, 8, 0, 10, 56, 108, 108, 104, 48, 107, 206, 204, 206, 122, 0 };
|
||||
/* ' */ static const uint8_t Font_Unifont_Bold_16_glyph_39[] = { 2, 4, 7, 2, 12, 253, 0 };
|
||||
/* ( */ static const uint8_t Font_Unifont_Bold_16_glyph_40[] = { 4, 12, 7, 2, 11, 54, 108, 204, 204, 198, 99, 0 };
|
||||
/* ) */ static const uint8_t Font_Unifont_Bold_16_glyph_41[] = { 4, 12, 7, 0, 11, 198, 99, 51, 51, 54, 108, 0 };
|
||||
/* * */ static const uint8_t Font_Unifont_Bold_16_glyph_42[] = { 7, 7, 8, 0, 8, 17, 37, 81, 197, 82, 68, 0 };
|
||||
/* + */ static const uint8_t Font_Unifont_Bold_16_glyph_43[] = { 7, 7, 8, 0, 8, 16, 32, 71, 241, 2, 4, 0 };
|
||||
/* , */ static const uint8_t Font_Unifont_Bold_16_glyph_44[] = { 3, 4, 7, 1, 2, 237, 224 };
|
||||
/* - */ static const uint8_t Font_Unifont_Bold_16_glyph_45[] = { 5, 1, 7, 0, 5, 248 };
|
||||
/* . */ static const uint8_t Font_Unifont_Bold_16_glyph_46[] = { 2, 2, 7, 2, 2, 240 };
|
||||
/* / */ static const uint8_t Font_Unifont_Bold_16_glyph_47[] = { 6, 10, 7, 0, 10, 12, 49, 132, 48, 194, 24, 195, 0 };
|
||||
/* 0 */ static const uint8_t Font_Unifont_Bold_16_glyph_48[] = { 7, 10, 8, 0, 10, 56, 219, 30, 125, 122, 249, 227, 108, 112 };
|
||||
/* 1 */ static const uint8_t Font_Unifont_Bold_16_glyph_49[] = { 6, 10, 7, 0, 10, 51, 195, 12, 48, 195, 12, 51, 240 };
|
||||
/* 2 */ static const uint8_t Font_Unifont_Bold_16_glyph_50[] = { 7, 10, 8, 0, 10, 125, 143, 24, 48, 195, 12, 112, 193, 252 };
|
||||
/* 3 */ static const uint8_t Font_Unifont_Bold_16_glyph_51[] = { 7, 10, 8, 0, 10, 125, 143, 24, 49, 192, 193, 227, 198, 248 };
|
||||
/* 4 */ static const uint8_t Font_Unifont_Bold_16_glyph_52[] = { 7, 10, 8, 0, 10, 12, 120, 179, 100, 217, 191, 134, 12, 24 };
|
||||
/* 5 */ static const uint8_t Font_Unifont_Bold_16_glyph_53[] = { 7, 10, 8, 0, 10, 255, 131, 6, 15, 193, 193, 227, 198, 248 };
|
||||
/* 6 */ static const uint8_t Font_Unifont_Bold_16_glyph_54[] = { 7, 10, 8, 0, 10, 60, 195, 6, 15, 216, 241, 227, 198, 248 };
|
||||
/* 7 */ static const uint8_t Font_Unifont_Bold_16_glyph_55[] = { 6, 10, 7, 0, 10, 252, 48, 195, 24, 99, 12, 97, 128 };
|
||||
/* 8 */ static const uint8_t Font_Unifont_Bold_16_glyph_56[] = { 7, 10, 8, 0, 10, 125, 143, 30, 55, 216, 241, 227, 198, 248 };
|
||||
/* 9 */ static const uint8_t Font_Unifont_Bold_16_glyph_57[] = { 7, 10, 8, 0, 10, 125, 143, 30, 60, 111, 193, 131, 12, 240 };
|
||||
/* : */ static const uint8_t Font_Unifont_Bold_16_glyph_58[] = { 2, 7, 7, 2, 8, 240, 60 };
|
||||
/* ; */ static const uint8_t Font_Unifont_Bold_16_glyph_59[] = { 2, 9, 7, 2, 8, 240, 61, 128 };
|
||||
/* < */ static const uint8_t Font_Unifont_Bold_16_glyph_60[] = { 6, 9, 7, 0, 9, 12, 99, 24, 193, 131, 6, 12 };
|
||||
/* = */ static const uint8_t Font_Unifont_Bold_16_glyph_61[] = { 6, 5, 7, 0, 7, 252, 0, 0, 252 };
|
||||
/* > */ static const uint8_t Font_Unifont_Bold_16_glyph_62[] = { 6, 9, 7, 0, 9, 193, 131, 6, 12, 99, 24, 192 };
|
||||
/* ? */ static const uint8_t Font_Unifont_Bold_16_glyph_63[] = { 7, 10, 8, 0, 10, 125, 143, 24, 48, 195, 6, 0, 24, 48 };
|
||||
/* @ */ static const uint8_t Font_Unifont_Bold_16_glyph_64[] = { 7, 10, 8, 0, 10, 60, 134, 109, 187, 118, 237, 205, 64, 124 };
|
||||
/* A */ static const uint8_t Font_Unifont_Bold_16_glyph_65[] = { 7, 10, 8, 0, 10, 56, 249, 182, 60, 120, 255, 227, 199, 140 };
|
||||
/* B */ static const uint8_t Font_Unifont_Bold_16_glyph_66[] = { 7, 10, 8, 0, 10, 253, 143, 30, 63, 216, 241, 227, 199, 248 };
|
||||
/* C */ static const uint8_t Font_Unifont_Bold_16_glyph_67[] = { 7, 10, 8, 0, 10, 125, 143, 30, 12, 24, 48, 99, 198, 248 };
|
||||
/* D */ static const uint8_t Font_Unifont_Bold_16_glyph_68[] = { 7, 10, 8, 0, 10, 241, 155, 30, 60, 120, 241, 227, 205, 224 };
|
||||
/* E */ static const uint8_t Font_Unifont_Bold_16_glyph_69[] = { 6, 10, 7, 0, 10, 255, 12, 48, 251, 12, 48, 195, 240 };
|
||||
/* F */ static const uint8_t Font_Unifont_Bold_16_glyph_70[] = { 6, 10, 7, 0, 10, 255, 12, 48, 195, 236, 48, 195, 0 };
|
||||
/* G */ static const uint8_t Font_Unifont_Bold_16_glyph_71[] = { 7, 10, 8, 0, 10, 125, 143, 30, 12, 27, 241, 227, 206, 236 };
|
||||
/* H */ static const uint8_t Font_Unifont_Bold_16_glyph_72[] = { 7, 10, 8, 0, 10, 199, 143, 30, 63, 248, 241, 227, 199, 140 };
|
||||
/* I */ static const uint8_t Font_Unifont_Bold_16_glyph_73[] = { 6, 10, 7, 0, 10, 252, 195, 12, 48, 195, 12, 51, 240 };
|
||||
/* J */ static const uint8_t Font_Unifont_Bold_16_glyph_74[] = { 7, 10, 8, 0, 10, 62, 24, 48, 96, 193, 131, 102, 204, 240 };
|
||||
/* K */ static const uint8_t Font_Unifont_Bold_16_glyph_75[] = { 7, 10, 8, 0, 10, 199, 143, 54, 207, 28, 62, 110, 207, 140 };
|
||||
/* L */ static const uint8_t Font_Unifont_Bold_16_glyph_76[] = { 6, 10, 7, 0, 10, 195, 12, 48, 195, 12, 48, 195, 240 };
|
||||
/* M */ static const uint8_t Font_Unifont_Bold_16_glyph_77[] = { 7, 10, 8, 0, 10, 131, 143, 31, 127, 250, 245, 227, 199, 140 };
|
||||
/* N */ static const uint8_t Font_Unifont_Bold_16_glyph_78[] = { 7, 10, 8, 0, 10, 199, 207, 158, 189, 122, 245, 231, 207, 140 };
|
||||
/* O */ static const uint8_t Font_Unifont_Bold_16_glyph_79[] = { 7, 10, 8, 0, 10, 125, 143, 30, 60, 120, 241, 227, 198, 248 };
|
||||
/* P */ static const uint8_t Font_Unifont_Bold_16_glyph_80[] = { 7, 10, 8, 0, 10, 253, 143, 30, 60, 127, 176, 96, 193, 128 };
|
||||
/* Q */ static const uint8_t Font_Unifont_Bold_16_glyph_81[] = { 7, 11, 8, 0, 10, 125, 143, 30, 60, 120, 241, 235, 238, 112, 24 };
|
||||
/* R */ static const uint8_t Font_Unifont_Bold_16_glyph_82[] = { 7, 10, 8, 0, 10, 253, 143, 30, 60, 127, 182, 102, 199, 140 };
|
||||
/* S */ static const uint8_t Font_Unifont_Bold_16_glyph_83[] = { 7, 10, 8, 0, 10, 125, 143, 31, 7, 135, 131, 227, 198, 248 };
|
||||
/* T */ static const uint8_t Font_Unifont_Bold_16_glyph_84[] = { 7, 10, 8, 0, 10, 254, 48, 96, 193, 131, 6, 12, 24, 48 };
|
||||
/* U */ static const uint8_t Font_Unifont_Bold_16_glyph_85[] = { 7, 10, 8, 0, 10, 199, 143, 30, 60, 120, 241, 227, 238, 248 };
|
||||
/* V */ static const uint8_t Font_Unifont_Bold_16_glyph_86[] = { 7, 10, 8, 0, 10, 199, 143, 26, 38, 205, 155, 20, 40, 112 };
|
||||
/* W */ static const uint8_t Font_Unifont_Bold_16_glyph_87[] = { 7, 10, 8, 0, 10, 199, 143, 30, 189, 122, 245, 255, 238, 136 };
|
||||
/* X */ static const uint8_t Font_Unifont_Bold_16_glyph_88[] = { 7, 10, 8, 0, 10, 199, 141, 179, 99, 135, 27, 54, 199, 140 };
|
||||
/* Y */ static const uint8_t Font_Unifont_Bold_16_glyph_89[] = { 6, 10, 7, 0, 10, 207, 60, 243, 73, 227, 12, 48, 192 };
|
||||
/* Z */ static const uint8_t Font_Unifont_Bold_16_glyph_90[] = { 7, 10, 8, 0, 10, 254, 12, 56, 225, 135, 28, 112, 193, 252 };
|
||||
/* [ */ static const uint8_t Font_Unifont_Bold_16_glyph_91[] = { 4, 12, 7, 2, 11, 252, 204, 204, 204, 204, 207, 0 };
|
||||
/* \ */ static const uint8_t Font_Unifont_Bold_16_glyph_92[] = { 6, 10, 7, 0, 10, 195, 6, 8, 48, 193, 6, 12, 48 };
|
||||
/* ] */ static const uint8_t Font_Unifont_Bold_16_glyph_93[] = { 4, 12, 7, 0, 11, 243, 51, 51, 51, 51, 63, 0 };
|
||||
/* ^ */ static const uint8_t Font_Unifont_Bold_16_glyph_94[] = { 7, 3, 8, 0, 12, 56, 219, 24 };
|
||||
/* _ */ static const uint8_t Font_Unifont_Bold_16_glyph_95[] = { 7, 1, 7, 0, 0, 254 };
|
||||
/* ` */ static const uint8_t Font_Unifont_Bold_16_glyph_96[] = { 4, 3, 7, 0, 13, 198, 48 };
|
||||
/* a */ static const uint8_t Font_Unifont_Bold_16_glyph_97[] = { 7, 8, 8, 0, 8, 125, 140, 27, 252, 120, 243, 187, 0 };
|
||||
/* b */ static const uint8_t Font_Unifont_Bold_16_glyph_98[] = { 7, 11, 8, 0, 11, 193, 131, 6, 238, 120, 241, 227, 199, 207, 112 };
|
||||
/* c */ static const uint8_t Font_Unifont_Bold_16_glyph_99[] = { 7, 8, 8, 0, 8, 125, 143, 30, 12, 24, 241, 190, 0 };
|
||||
/* d */ static const uint8_t Font_Unifont_Bold_16_glyph_100[] = { 7, 11, 8, 0, 11, 6, 12, 25, 188, 248, 241, 227, 199, 156, 216 };
|
||||
/* e */ static const uint8_t Font_Unifont_Bold_16_glyph_101[] = { 7, 8, 8, 0, 8, 125, 143, 31, 252, 24, 241, 190, 0 };
|
||||
/* f */ static const uint8_t Font_Unifont_Bold_16_glyph_102[] = { 7, 11, 8, 0, 11, 30, 96, 193, 143, 230, 12, 24, 48, 97, 240 };
|
||||
/* g */ static const uint8_t Font_Unifont_Bold_16_glyph_103[] = { 7, 11, 8, 0, 9, 2, 247, 54, 108, 207, 8, 62, 207, 141, 240 };
|
||||
/* h */ static const uint8_t Font_Unifont_Bold_16_glyph_104[] = { 7, 11, 8, 0, 11, 193, 131, 6, 238, 120, 241, 227, 199, 143, 24 };
|
||||
/* i */ static const uint8_t Font_Unifont_Bold_16_glyph_105[] = { 6, 11, 7, 0, 11, 48, 192, 60, 48, 195, 12, 48, 207, 192 };
|
||||
/* j */ static const uint8_t Font_Unifont_Bold_16_glyph_106[] = { 6, 13, 8, 0, 11, 24, 96, 31, 12, 48, 195, 12, 60, 246, 112 };
|
||||
/* k */ static const uint8_t Font_Unifont_Bold_16_glyph_107[] = { 7, 11, 8, 0, 11, 193, 131, 6, 60, 251, 60, 120, 217, 159, 24 };
|
||||
/* l */ static const uint8_t Font_Unifont_Bold_16_glyph_108[] = { 6, 11, 7, 0, 11, 240, 195, 12, 48, 195, 12, 48, 207, 192 };
|
||||
/* m */ static const uint8_t Font_Unifont_Bold_16_glyph_109[] = { 7, 8, 8, 0, 8, 237, 175, 94, 189, 122, 245, 235, 0 };
|
||||
/* n */ static const uint8_t Font_Unifont_Bold_16_glyph_110[] = { 7, 8, 8, 0, 8, 221, 207, 30, 60, 120, 241, 227, 0 };
|
||||
/* o */ static const uint8_t Font_Unifont_Bold_16_glyph_111[] = { 7, 8, 8, 0, 8, 125, 143, 30, 60, 120, 241, 190, 0 };
|
||||
/* p */ static const uint8_t Font_Unifont_Bold_16_glyph_112[] = { 7, 10, 8, 0, 8, 221, 207, 30, 60, 120, 249, 238, 193, 128 };
|
||||
/* q */ static const uint8_t Font_Unifont_Bold_16_glyph_113[] = { 7, 10, 8, 0, 8, 119, 159, 30, 60, 120, 243, 187, 6, 12 };
|
||||
/* r */ static const uint8_t Font_Unifont_Bold_16_glyph_114[] = { 7, 8, 8, 0, 8, 221, 207, 30, 12, 24, 48, 96, 0 };
|
||||
/* s */ static const uint8_t Font_Unifont_Bold_16_glyph_115[] = { 7, 8, 8, 0, 8, 125, 143, 27, 129, 216, 241, 190, 0 };
|
||||
/* t */ static const uint8_t Font_Unifont_Bold_16_glyph_116[] = { 7, 10, 8, 0, 10, 48, 96, 199, 243, 6, 12, 24, 48, 60 };
|
||||
/* u */ static const uint8_t Font_Unifont_Bold_16_glyph_117[] = { 7, 8, 8, 0, 8, 199, 143, 30, 60, 120, 243, 187, 0 };
|
||||
/* v */ static const uint8_t Font_Unifont_Bold_16_glyph_118[] = { 7, 8, 8, 0, 8, 199, 143, 26, 38, 205, 142, 28, 0 };
|
||||
/* w */ static const uint8_t Font_Unifont_Bold_16_glyph_119[] = { 7, 8, 8, 0, 8, 199, 175, 94, 189, 122, 245, 182, 0 };
|
||||
/* x */ static const uint8_t Font_Unifont_Bold_16_glyph_120[] = { 7, 8, 8, 0, 8, 199, 141, 177, 195, 141, 177, 227, 0 };
|
||||
/* y */ static const uint8_t Font_Unifont_Bold_16_glyph_121[] = { 7, 10, 8, 0, 8, 199, 143, 30, 60, 109, 205, 131, 6, 248 };
|
||||
/* z */ static const uint8_t Font_Unifont_Bold_16_glyph_122[] = { 7, 8, 8, 0, 8, 254, 12, 56, 227, 142, 56, 127, 0 };
|
||||
/* { */ static const uint8_t Font_Unifont_Bold_16_glyph_123[] = { 5, 13, 7, 1, 11, 59, 24, 99, 51, 12, 49, 152, 195, 128 };
|
||||
/* | */ static const uint8_t Font_Unifont_Bold_16_glyph_124[] = { 2, 14, 7, 2, 12, 255, 255, 255, 240 };
|
||||
/* } */ static const uint8_t Font_Unifont_Bold_16_glyph_125[] = { 5, 13, 7, 0, 11, 225, 140, 198, 24, 102, 99, 12, 110, 0 };
|
||||
/* ~ */ static const uint8_t Font_Unifont_Bold_16_glyph_126[] = { 7, 3, 8, 0, 11, 99, 118, 48 };
|
||||
|
||||
const uint8_t Font_Unifont_Bold_16_glyph_nonprintable[] = { 7, 10, 8, 0, 10, 130, 112, 231, 207, 60, 249, 255, 231, 207 };
|
||||
|
||||
const uint8_t * const Font_Unifont_Bold_16[126 + 1 - 32] = {
|
||||
Font_Unifont_Bold_16_glyph_32,
|
||||
Font_Unifont_Bold_16_glyph_33,
|
||||
Font_Unifont_Bold_16_glyph_34,
|
||||
Font_Unifont_Bold_16_glyph_35,
|
||||
Font_Unifont_Bold_16_glyph_36,
|
||||
Font_Unifont_Bold_16_glyph_37,
|
||||
Font_Unifont_Bold_16_glyph_38,
|
||||
Font_Unifont_Bold_16_glyph_39,
|
||||
Font_Unifont_Bold_16_glyph_40,
|
||||
Font_Unifont_Bold_16_glyph_41,
|
||||
Font_Unifont_Bold_16_glyph_42,
|
||||
Font_Unifont_Bold_16_glyph_43,
|
||||
Font_Unifont_Bold_16_glyph_44,
|
||||
Font_Unifont_Bold_16_glyph_45,
|
||||
Font_Unifont_Bold_16_glyph_46,
|
||||
Font_Unifont_Bold_16_glyph_47,
|
||||
Font_Unifont_Bold_16_glyph_48,
|
||||
Font_Unifont_Bold_16_glyph_49,
|
||||
Font_Unifont_Bold_16_glyph_50,
|
||||
Font_Unifont_Bold_16_glyph_51,
|
||||
Font_Unifont_Bold_16_glyph_52,
|
||||
Font_Unifont_Bold_16_glyph_53,
|
||||
Font_Unifont_Bold_16_glyph_54,
|
||||
Font_Unifont_Bold_16_glyph_55,
|
||||
Font_Unifont_Bold_16_glyph_56,
|
||||
Font_Unifont_Bold_16_glyph_57,
|
||||
Font_Unifont_Bold_16_glyph_58,
|
||||
Font_Unifont_Bold_16_glyph_59,
|
||||
Font_Unifont_Bold_16_glyph_60,
|
||||
Font_Unifont_Bold_16_glyph_61,
|
||||
Font_Unifont_Bold_16_glyph_62,
|
||||
Font_Unifont_Bold_16_glyph_63,
|
||||
Font_Unifont_Bold_16_glyph_64,
|
||||
Font_Unifont_Bold_16_glyph_65,
|
||||
Font_Unifont_Bold_16_glyph_66,
|
||||
Font_Unifont_Bold_16_glyph_67,
|
||||
Font_Unifont_Bold_16_glyph_68,
|
||||
Font_Unifont_Bold_16_glyph_69,
|
||||
Font_Unifont_Bold_16_glyph_70,
|
||||
Font_Unifont_Bold_16_glyph_71,
|
||||
Font_Unifont_Bold_16_glyph_72,
|
||||
Font_Unifont_Bold_16_glyph_73,
|
||||
Font_Unifont_Bold_16_glyph_74,
|
||||
Font_Unifont_Bold_16_glyph_75,
|
||||
Font_Unifont_Bold_16_glyph_76,
|
||||
Font_Unifont_Bold_16_glyph_77,
|
||||
Font_Unifont_Bold_16_glyph_78,
|
||||
Font_Unifont_Bold_16_glyph_79,
|
||||
Font_Unifont_Bold_16_glyph_80,
|
||||
Font_Unifont_Bold_16_glyph_81,
|
||||
Font_Unifont_Bold_16_glyph_82,
|
||||
Font_Unifont_Bold_16_glyph_83,
|
||||
Font_Unifont_Bold_16_glyph_84,
|
||||
Font_Unifont_Bold_16_glyph_85,
|
||||
Font_Unifont_Bold_16_glyph_86,
|
||||
Font_Unifont_Bold_16_glyph_87,
|
||||
Font_Unifont_Bold_16_glyph_88,
|
||||
Font_Unifont_Bold_16_glyph_89,
|
||||
Font_Unifont_Bold_16_glyph_90,
|
||||
Font_Unifont_Bold_16_glyph_91,
|
||||
Font_Unifont_Bold_16_glyph_92,
|
||||
Font_Unifont_Bold_16_glyph_93,
|
||||
Font_Unifont_Bold_16_glyph_94,
|
||||
Font_Unifont_Bold_16_glyph_95,
|
||||
Font_Unifont_Bold_16_glyph_96,
|
||||
Font_Unifont_Bold_16_glyph_97,
|
||||
Font_Unifont_Bold_16_glyph_98,
|
||||
Font_Unifont_Bold_16_glyph_99,
|
||||
Font_Unifont_Bold_16_glyph_100,
|
||||
Font_Unifont_Bold_16_glyph_101,
|
||||
Font_Unifont_Bold_16_glyph_102,
|
||||
Font_Unifont_Bold_16_glyph_103,
|
||||
Font_Unifont_Bold_16_glyph_104,
|
||||
Font_Unifont_Bold_16_glyph_105,
|
||||
Font_Unifont_Bold_16_glyph_106,
|
||||
Font_Unifont_Bold_16_glyph_107,
|
||||
Font_Unifont_Bold_16_glyph_108,
|
||||
Font_Unifont_Bold_16_glyph_109,
|
||||
Font_Unifont_Bold_16_glyph_110,
|
||||
Font_Unifont_Bold_16_glyph_111,
|
||||
Font_Unifont_Bold_16_glyph_112,
|
||||
Font_Unifont_Bold_16_glyph_113,
|
||||
Font_Unifont_Bold_16_glyph_114,
|
||||
Font_Unifont_Bold_16_glyph_115,
|
||||
Font_Unifont_Bold_16_glyph_116,
|
||||
Font_Unifont_Bold_16_glyph_117,
|
||||
Font_Unifont_Bold_16_glyph_118,
|
||||
Font_Unifont_Bold_16_glyph_119,
|
||||
Font_Unifont_Bold_16_glyph_120,
|
||||
Font_Unifont_Bold_16_glyph_121,
|
||||
Font_Unifont_Bold_16_glyph_122,
|
||||
Font_Unifont_Bold_16_glyph_123,
|
||||
Font_Unifont_Bold_16_glyph_124,
|
||||
Font_Unifont_Bold_16_glyph_125,
|
||||
Font_Unifont_Bold_16_glyph_126,
|
||||
};
|
10
core/embed/lib/fonts/font_unifont_bold_16.h
Normal file
@ -0,0 +1,10 @@
|
||||
#include <stdint.h>
|
||||
|
||||
#if TREZOR_FONT_BPP != 1
|
||||
#error Wrong TREZOR_FONT_BPP (expected 1)
|
||||
#endif
|
||||
#define Font_Unifont_Bold_16_HEIGHT 12 // <--- 12 from 16
|
||||
#define Font_Unifont_Bold_16_MAX_HEIGHT 12 // <--- 12 from 15
|
||||
#define Font_Unifont_Bold_16_BASELINE 2
|
||||
extern const uint8_t* const Font_Unifont_Bold_16[126 + 1 - 32];
|
||||
extern const uint8_t Font_Unifont_Bold_16_glyph_nonprintable[];
|
207
core/embed/lib/fonts/font_unifont_regular_16.c
Normal file
@ -0,0 +1,207 @@
|
||||
#include <stdint.h>
|
||||
|
||||
// clang-format off
|
||||
|
||||
// - the first two bytes are width and height of the glyph
|
||||
// - the third, fourth and fifth bytes are advance, bearingX and bearingY of the horizontal metrics of the glyph
|
||||
// - the rest is packed 1-bit glyph data
|
||||
|
||||
// MANUAL CHANGES!
|
||||
// In cases where the width and advance were the same (usually 7 and 7), increasing
|
||||
// the advance to 8, so that these wide letters do not collide with the following one.
|
||||
|
||||
/* */ static const uint8_t Font_Unifont_Regular_16_glyph_32[] = { 0, 0, 8, 0, 0 };
|
||||
/* ! */ static const uint8_t Font_Unifont_Regular_16_glyph_33[] = { 1, 10, 7, 3, 10, 254, 192 };
|
||||
/* " */ static const uint8_t Font_Unifont_Regular_16_glyph_34[] = { 5, 4, 7, 1, 12, 140, 99, 16 };
|
||||
/* # */ static const uint8_t Font_Unifont_Regular_16_glyph_35[] = { 6, 10, 7, 0, 10, 36, 146, 127, 73, 47, 228, 146, 64 };
|
||||
/* $ */ static const uint8_t Font_Unifont_Regular_16_glyph_36[] = { 7, 10, 8, 0, 10, 16, 250, 76, 135, 3, 132, 201, 124, 32 }; // < --- advanced changed from 7 to 8
|
||||
/* % */ static const uint8_t Font_Unifont_Regular_16_glyph_37[] = { 7, 10, 8, 0, 10, 99, 42, 83, 65, 2, 11, 41, 83, 24 }; // < --- advanced changed from 7 to 8
|
||||
/* & */ static const uint8_t Font_Unifont_Regular_16_glyph_38[] = { 7, 10, 8, 0, 10, 56, 137, 17, 67, 10, 98, 194, 140, 228 }; // < --- advanced changed from 7 to 8
|
||||
/* ' */ static const uint8_t Font_Unifont_Regular_16_glyph_39[] = { 1, 4, 7, 3, 12, 240 };
|
||||
/* ( */ static const uint8_t Font_Unifont_Regular_16_glyph_40[] = { 3, 12, 7, 2, 11, 41, 73, 36, 137, 16 };
|
||||
/* ) */ static const uint8_t Font_Unifont_Regular_16_glyph_41[] = { 3, 12, 7, 1, 11, 137, 18, 73, 41, 64 };
|
||||
/* * */ static const uint8_t Font_Unifont_Regular_16_glyph_42[] = { 7, 7, 8, 0, 8, 17, 37, 81, 197, 82, 68, 0 }; // < --- advanced changed from 7 to 8
|
||||
/* + */ static const uint8_t Font_Unifont_Regular_16_glyph_43[] = { 7, 7, 8, 0, 8, 16, 32, 71, 241, 2, 4, 0 }; // < --- advanced changed from 7 to 8
|
||||
/* , */ static const uint8_t Font_Unifont_Regular_16_glyph_44[] = { 2, 4, 7, 2, 2, 214, 0 };
|
||||
/* - */ static const uint8_t Font_Unifont_Regular_16_glyph_45[] = { 4, 1, 7, 1, 5, 240 };
|
||||
/* . */ static const uint8_t Font_Unifont_Regular_16_glyph_46[] = { 2, 2, 7, 2, 2, 240 };
|
||||
/* / */ static const uint8_t Font_Unifont_Regular_16_glyph_47[] = { 6, 10, 7, 0, 10, 4, 16, 132, 16, 130, 16, 130, 0 };
|
||||
/* 0 */ static const uint8_t Font_Unifont_Regular_16_glyph_48[] = { 6, 10, 7, 0, 10, 49, 40, 99, 150, 156, 97, 72, 192 };
|
||||
/* 1 */ static const uint8_t Font_Unifont_Regular_16_glyph_49[] = { 5, 10, 7, 1, 10, 35, 40, 66, 16, 132, 39, 192 };
|
||||
/* 2 */ static const uint8_t Font_Unifont_Regular_16_glyph_50[] = { 6, 10, 7, 0, 10, 122, 24, 65, 24, 132, 32, 131, 240 };
|
||||
/* 3 */ static const uint8_t Font_Unifont_Regular_16_glyph_51[] = { 6, 10, 7, 0, 10, 122, 24, 65, 56, 16, 97, 133, 224 };
|
||||
/* 4 */ static const uint8_t Font_Unifont_Regular_16_glyph_52[] = { 6, 10, 7, 0, 10, 8, 98, 146, 138, 47, 194, 8, 32 };
|
||||
/* 5 */ static const uint8_t Font_Unifont_Regular_16_glyph_53[] = { 6, 10, 7, 0, 10, 254, 8, 32, 248, 16, 65, 133, 224 };
|
||||
/* 6 */ static const uint8_t Font_Unifont_Regular_16_glyph_54[] = { 6, 10, 7, 0, 10, 57, 8, 32, 250, 24, 97, 133, 224 };
|
||||
/* 7 */ static const uint8_t Font_Unifont_Regular_16_glyph_55[] = { 6, 10, 7, 0, 10, 252, 16, 66, 8, 33, 4, 16, 64 };
|
||||
/* 8 */ static const uint8_t Font_Unifont_Regular_16_glyph_56[] = { 6, 10, 7, 0, 10, 122, 24, 97, 122, 24, 97, 133, 224 };
|
||||
/* 9 */ static const uint8_t Font_Unifont_Regular_16_glyph_57[] = { 6, 10, 7, 0, 10, 122, 24, 97, 124, 16, 65, 9, 192 };
|
||||
/* : */ static const uint8_t Font_Unifont_Regular_16_glyph_58[] = { 2, 7, 7, 2, 8, 240, 60 };
|
||||
/* ; */ static const uint8_t Font_Unifont_Regular_16_glyph_59[] = { 2, 9, 7, 2, 8, 240, 53, 128 };
|
||||
/* < */ static const uint8_t Font_Unifont_Regular_16_glyph_60[] = { 5, 9, 7, 1, 9, 8, 136, 136, 32, 130, 8 };
|
||||
/* = */ static const uint8_t Font_Unifont_Regular_16_glyph_61[] = { 6, 5, 7, 0, 7, 252, 0, 0, 252 };
|
||||
/* > */ static const uint8_t Font_Unifont_Regular_16_glyph_62[] = { 5, 9, 7, 0, 9, 130, 8, 32, 136, 136, 128 };
|
||||
/* ? */ static const uint8_t Font_Unifont_Regular_16_glyph_63[] = { 6, 10, 7, 0, 10, 122, 24, 65, 8, 65, 0, 16, 64 };
|
||||
/* @ */ static const uint8_t Font_Unifont_Regular_16_glyph_64[] = { 6, 10, 7, 0, 10, 57, 25, 107, 166, 154, 103, 64, 240 };
|
||||
/* A */ static const uint8_t Font_Unifont_Regular_16_glyph_65[] = { 6, 10, 7, 0, 10, 49, 36, 161, 135, 248, 97, 134, 16 };
|
||||
/* B */ static const uint8_t Font_Unifont_Regular_16_glyph_66[] = { 6, 10, 7, 0, 10, 250, 24, 97, 250, 24, 97, 135, 224 };
|
||||
/* C */ static const uint8_t Font_Unifont_Regular_16_glyph_67[] = { 6, 10, 7, 0, 10, 122, 24, 96, 130, 8, 33, 133, 224 };
|
||||
/* D */ static const uint8_t Font_Unifont_Regular_16_glyph_68[] = { 6, 10, 7, 0, 10, 242, 40, 97, 134, 24, 97, 139, 192 };
|
||||
/* E */ static const uint8_t Font_Unifont_Regular_16_glyph_69[] = { 6, 10, 7, 0, 10, 254, 8, 32, 250, 8, 32, 131, 240 };
|
||||
/* F */ static const uint8_t Font_Unifont_Regular_16_glyph_70[] = { 6, 10, 7, 0, 10, 254, 8, 32, 250, 8, 32, 130, 0 };
|
||||
/* G */ static const uint8_t Font_Unifont_Regular_16_glyph_71[] = { 6, 10, 7, 0, 10, 122, 24, 96, 130, 120, 97, 141, 208 };
|
||||
/* H */ static const uint8_t Font_Unifont_Regular_16_glyph_72[] = { 6, 10, 7, 0, 10, 134, 24, 97, 254, 24, 97, 134, 16 };
|
||||
/* I */ static const uint8_t Font_Unifont_Regular_16_glyph_73[] = { 5, 10, 7, 1, 10, 249, 8, 66, 16, 132, 39, 192 };
|
||||
/* J */ static const uint8_t Font_Unifont_Regular_16_glyph_74[] = { 7, 10, 8, 0, 10, 62, 16, 32, 64, 129, 2, 68, 136, 224 }; // < --- advanced changed from 7 to 8
|
||||
/* K */ static const uint8_t Font_Unifont_Regular_16_glyph_75[] = { 6, 10, 7, 0, 10, 134, 41, 40, 195, 10, 36, 138, 16 };
|
||||
/* L */ static const uint8_t Font_Unifont_Regular_16_glyph_76[] = { 6, 10, 7, 0, 10, 130, 8, 32, 130, 8, 32, 131, 240 };
|
||||
/* M */ static const uint8_t Font_Unifont_Regular_16_glyph_77[] = { 6, 10, 7, 0, 10, 134, 28, 243, 182, 216, 97, 134, 16 };
|
||||
/* N */ static const uint8_t Font_Unifont_Regular_16_glyph_78[] = { 6, 10, 7, 0, 10, 135, 28, 105, 166, 89, 99, 142, 16 };
|
||||
/* O */ static const uint8_t Font_Unifont_Regular_16_glyph_79[] = { 6, 10, 7, 0, 10, 122, 24, 97, 134, 24, 97, 133, 224 };
|
||||
/* P */ static const uint8_t Font_Unifont_Regular_16_glyph_80[] = { 6, 10, 7, 0, 10, 250, 24, 97, 250, 8, 32, 130, 0 };
|
||||
/* Q */ static const uint8_t Font_Unifont_Regular_16_glyph_81[] = { 7, 11, 8, 0, 10, 121, 10, 20, 40, 80, 161, 90, 204, 240, 24 }; // < --- advanced changed from 7 to 8
|
||||
/* R */ static const uint8_t Font_Unifont_Regular_16_glyph_82[] = { 6, 10, 7, 0, 10, 250, 24, 97, 250, 72, 162, 134, 16 };
|
||||
/* S */ static const uint8_t Font_Unifont_Regular_16_glyph_83[] = { 6, 10, 7, 0, 10, 122, 24, 96, 96, 96, 97, 133, 224 };
|
||||
/* T */ static const uint8_t Font_Unifont_Regular_16_glyph_84[] = { 7, 10, 8, 0, 10, 254, 32, 64, 129, 2, 4, 8, 16, 32 }; // < --- advanced changed from 7 to 8
|
||||
/* U */ static const uint8_t Font_Unifont_Regular_16_glyph_85[] = { 6, 10, 7, 0, 10, 134, 24, 97, 134, 24, 97, 133, 224 };
|
||||
/* V */ static const uint8_t Font_Unifont_Regular_16_glyph_86[] = { 7, 10, 8, 0, 10, 131, 6, 10, 36, 72, 138, 20, 16, 32 }; // < --- advanced changed from 7 to 8
|
||||
/* W */ static const uint8_t Font_Unifont_Regular_16_glyph_87[] = { 6, 10, 7, 0, 10, 134, 24, 97, 182, 220, 243, 134, 16 };
|
||||
/* X */ static const uint8_t Font_Unifont_Regular_16_glyph_88[] = { 6, 10, 7, 0, 10, 134, 20, 146, 48, 196, 146, 134, 16 };
|
||||
/* Y */ static const uint8_t Font_Unifont_Regular_16_glyph_89[] = { 7, 10, 8, 0, 10, 131, 5, 18, 34, 130, 4, 8, 16, 32 }; // < --- advanced changed from 7 to 8
|
||||
/* Z */ static const uint8_t Font_Unifont_Regular_16_glyph_90[] = { 6, 10, 7, 0, 10, 252, 16, 66, 16, 132, 32, 131, 240 };
|
||||
/* [ */ static const uint8_t Font_Unifont_Regular_16_glyph_91[] = { 3, 12, 7, 3, 11, 242, 73, 36, 146, 112 };
|
||||
/* \ */ static const uint8_t Font_Unifont_Regular_16_glyph_92[] = { 6, 10, 7, 0, 10, 130, 4, 8, 32, 65, 2, 4, 16 };
|
||||
/* ] */ static const uint8_t Font_Unifont_Regular_16_glyph_93[] = { 3, 12, 7, 0, 11, 228, 146, 73, 36, 240 };
|
||||
/* ^ */ static const uint8_t Font_Unifont_Regular_16_glyph_94[] = { 6, 3, 7, 0, 12, 49, 40, 64 };
|
||||
/* _ */ static const uint8_t Font_Unifont_Regular_16_glyph_95[] = { 7, 1, 8, 0, 0, 254 }; // < --- advanced changed from 7 to 8
|
||||
/* ` */ static const uint8_t Font_Unifont_Regular_16_glyph_96[] = { 3, 3, 7, 1, 13, 136, 128 };
|
||||
/* a */ static const uint8_t Font_Unifont_Regular_16_glyph_97[] = { 6, 8, 7, 0, 8, 122, 16, 95, 134, 24, 221, 0 };
|
||||
/* b */ static const uint8_t Font_Unifont_Regular_16_glyph_98[] = { 6, 11, 7, 0, 11, 130, 8, 46, 198, 24, 97, 135, 27, 128 };
|
||||
/* c */ static const uint8_t Font_Unifont_Regular_16_glyph_99[] = { 6, 8, 7, 0, 8, 122, 24, 32, 130, 8, 94, 0 };
|
||||
/* d */ static const uint8_t Font_Unifont_Regular_16_glyph_100[] = { 6, 11, 7, 0, 11, 4, 16, 93, 142, 24, 97, 134, 55, 64 };
|
||||
/* e */ static const uint8_t Font_Unifont_Regular_16_glyph_101[] = { 6, 8, 7, 0, 8, 122, 24, 127, 130, 8, 94, 0 };
|
||||
/* f */ static const uint8_t Font_Unifont_Regular_16_glyph_102[] = { 5, 11, 7, 0, 11, 25, 8, 79, 144, 132, 33, 8 };
|
||||
/* g */ static const uint8_t Font_Unifont_Regular_16_glyph_103[] = { 6, 11, 7, 0, 9, 5, 216, 162, 137, 196, 30, 134, 23, 128 };
|
||||
/* h */ static const uint8_t Font_Unifont_Regular_16_glyph_104[] = { 6, 11, 7, 0, 11, 130, 8, 46, 198, 24, 97, 134, 24, 64 };
|
||||
/* i */ static const uint8_t Font_Unifont_Regular_16_glyph_105[] = { 5, 11, 7, 1, 11, 33, 0, 194, 16, 132, 33, 62 };
|
||||
/* j */ static const uint8_t Font_Unifont_Regular_16_glyph_106[] = { 5, 13, 7, 0, 11, 8, 64, 48, 132, 33, 8, 67, 38, 0 };
|
||||
/* k */ static const uint8_t Font_Unifont_Regular_16_glyph_107[] = { 6, 11, 7, 0, 11, 130, 8, 34, 146, 140, 40, 146, 40, 64 };
|
||||
/* l */ static const uint8_t Font_Unifont_Regular_16_glyph_108[] = { 5, 11, 7, 1, 11, 97, 8, 66, 16, 132, 33, 62 };
|
||||
/* m */ static const uint8_t Font_Unifont_Regular_16_glyph_109[] = { 7, 8, 8, 0, 8, 237, 38, 76, 153, 50, 100, 201, 0 }; // < --- advanced changed from 7 to 8
|
||||
/* n */ static const uint8_t Font_Unifont_Regular_16_glyph_110[] = { 6, 8, 7, 0, 8, 187, 24, 97, 134, 24, 97, 0 };
|
||||
/* o */ static const uint8_t Font_Unifont_Regular_16_glyph_111[] = { 6, 8, 7, 0, 8, 122, 24, 97, 134, 24, 94, 0 };
|
||||
/* p */ static const uint8_t Font_Unifont_Regular_16_glyph_112[] = { 6, 10, 7, 0, 8, 187, 24, 97, 134, 28, 110, 130, 0 };
|
||||
/* q */ static const uint8_t Font_Unifont_Regular_16_glyph_113[] = { 6, 10, 7, 0, 8, 118, 56, 97, 134, 24, 221, 4, 16 };
|
||||
/* r */ static const uint8_t Font_Unifont_Regular_16_glyph_114[] = { 6, 8, 7, 0, 8, 187, 24, 96, 130, 8, 32, 0 };
|
||||
/* s */ static const uint8_t Font_Unifont_Regular_16_glyph_115[] = { 6, 8, 7, 0, 8, 122, 24, 24, 24, 24, 94, 0 };
|
||||
/* t */ static const uint8_t Font_Unifont_Regular_16_glyph_116[] = { 5, 10, 7, 0, 10, 33, 9, 242, 16, 132, 32, 192 };
|
||||
/* u */ static const uint8_t Font_Unifont_Regular_16_glyph_117[] = { 6, 8, 7, 0, 8, 134, 24, 97, 134, 24, 221, 0 };
|
||||
/* v */ static const uint8_t Font_Unifont_Regular_16_glyph_118[] = { 6, 8, 7, 0, 8, 134, 24, 82, 73, 35, 12, 0 };
|
||||
/* w */ static const uint8_t Font_Unifont_Regular_16_glyph_119[] = { 7, 8, 8, 0, 8, 131, 38, 76, 153, 50, 100, 182, 0 }; // < --- advanced changed from 7 to 8
|
||||
/* x */ static const uint8_t Font_Unifont_Regular_16_glyph_120[] = { 6, 8, 7, 0, 8, 134, 20, 140, 49, 40, 97, 0 };
|
||||
/* y */ static const uint8_t Font_Unifont_Regular_16_glyph_121[] = { 6, 10, 7, 0, 8, 134, 24, 97, 133, 51, 65, 5, 224 };
|
||||
/* z */ static const uint8_t Font_Unifont_Regular_16_glyph_122[] = { 6, 8, 7, 0, 8, 252, 16, 132, 33, 8, 63, 0 };
|
||||
/* { */ static const uint8_t Font_Unifont_Regular_16_glyph_123[] = { 4, 13, 7, 1, 11, 52, 66, 36, 132, 34, 68, 48 };
|
||||
/* | */ static const uint8_t Font_Unifont_Regular_16_glyph_124[] = { 1, 14, 7, 3, 12, 255, 252 };
|
||||
/* } */ static const uint8_t Font_Unifont_Regular_16_glyph_125[] = { 4, 13, 7, 1, 11, 194, 36, 66, 18, 68, 34, 192 };
|
||||
/* ~ */ static const uint8_t Font_Unifont_Regular_16_glyph_126[] = { 7, 3, 8, 0, 11, 99, 38, 48 }; // < --- advanced changed from 7 to 8
|
||||
|
||||
const uint8_t Font_Unifont_Regular_16_glyph_nonprintable[] = { 6, 10, 7, 0, 10, 133, 231, 190, 247, 190, 255, 239, 191 };
|
||||
|
||||
const uint8_t * const Font_Unifont_Regular_16[126 + 1 - 32] = {
|
||||
Font_Unifont_Regular_16_glyph_32,
|
||||
Font_Unifont_Regular_16_glyph_33,
|
||||
Font_Unifont_Regular_16_glyph_34,
|
||||
Font_Unifont_Regular_16_glyph_35,
|
||||
Font_Unifont_Regular_16_glyph_36,
|
||||
Font_Unifont_Regular_16_glyph_37,
|
||||
Font_Unifont_Regular_16_glyph_38,
|
||||
Font_Unifont_Regular_16_glyph_39,
|
||||
Font_Unifont_Regular_16_glyph_40,
|
||||
Font_Unifont_Regular_16_glyph_41,
|
||||
Font_Unifont_Regular_16_glyph_42,
|
||||
Font_Unifont_Regular_16_glyph_43,
|
||||
Font_Unifont_Regular_16_glyph_44,
|
||||
Font_Unifont_Regular_16_glyph_45,
|
||||
Font_Unifont_Regular_16_glyph_46,
|
||||
Font_Unifont_Regular_16_glyph_47,
|
||||
Font_Unifont_Regular_16_glyph_48,
|
||||
Font_Unifont_Regular_16_glyph_49,
|
||||
Font_Unifont_Regular_16_glyph_50,
|
||||
Font_Unifont_Regular_16_glyph_51,
|
||||
Font_Unifont_Regular_16_glyph_52,
|
||||
Font_Unifont_Regular_16_glyph_53,
|
||||
Font_Unifont_Regular_16_glyph_54,
|
||||
Font_Unifont_Regular_16_glyph_55,
|
||||
Font_Unifont_Regular_16_glyph_56,
|
||||
Font_Unifont_Regular_16_glyph_57,
|
||||
Font_Unifont_Regular_16_glyph_58,
|
||||
Font_Unifont_Regular_16_glyph_59,
|
||||
Font_Unifont_Regular_16_glyph_60,
|
||||
Font_Unifont_Regular_16_glyph_61,
|
||||
Font_Unifont_Regular_16_glyph_62,
|
||||
Font_Unifont_Regular_16_glyph_63,
|
||||
Font_Unifont_Regular_16_glyph_64,
|
||||
Font_Unifont_Regular_16_glyph_65,
|
||||
Font_Unifont_Regular_16_glyph_66,
|
||||
Font_Unifont_Regular_16_glyph_67,
|
||||
Font_Unifont_Regular_16_glyph_68,
|
||||
Font_Unifont_Regular_16_glyph_69,
|
||||
Font_Unifont_Regular_16_glyph_70,
|
||||
Font_Unifont_Regular_16_glyph_71,
|
||||
Font_Unifont_Regular_16_glyph_72,
|
||||
Font_Unifont_Regular_16_glyph_73,
|
||||
Font_Unifont_Regular_16_glyph_74,
|
||||
Font_Unifont_Regular_16_glyph_75,
|
||||
Font_Unifont_Regular_16_glyph_76,
|
||||
Font_Unifont_Regular_16_glyph_77,
|
||||
Font_Unifont_Regular_16_glyph_78,
|
||||
Font_Unifont_Regular_16_glyph_79,
|
||||
Font_Unifont_Regular_16_glyph_80,
|
||||
Font_Unifont_Regular_16_glyph_81,
|
||||
Font_Unifont_Regular_16_glyph_82,
|
||||
Font_Unifont_Regular_16_glyph_83,
|
||||
Font_Unifont_Regular_16_glyph_84,
|
||||
Font_Unifont_Regular_16_glyph_85,
|
||||
Font_Unifont_Regular_16_glyph_86,
|
||||
Font_Unifont_Regular_16_glyph_87,
|
||||
Font_Unifont_Regular_16_glyph_88,
|
||||
Font_Unifont_Regular_16_glyph_89,
|
||||
Font_Unifont_Regular_16_glyph_90,
|
||||
Font_Unifont_Regular_16_glyph_91,
|
||||
Font_Unifont_Regular_16_glyph_92,
|
||||
Font_Unifont_Regular_16_glyph_93,
|
||||
Font_Unifont_Regular_16_glyph_94,
|
||||
Font_Unifont_Regular_16_glyph_95,
|
||||
Font_Unifont_Regular_16_glyph_96,
|
||||
Font_Unifont_Regular_16_glyph_97,
|
||||
Font_Unifont_Regular_16_glyph_98,
|
||||
Font_Unifont_Regular_16_glyph_99,
|
||||
Font_Unifont_Regular_16_glyph_100,
|
||||
Font_Unifont_Regular_16_glyph_101,
|
||||
Font_Unifont_Regular_16_glyph_102,
|
||||
Font_Unifont_Regular_16_glyph_103,
|
||||
Font_Unifont_Regular_16_glyph_104,
|
||||
Font_Unifont_Regular_16_glyph_105,
|
||||
Font_Unifont_Regular_16_glyph_106,
|
||||
Font_Unifont_Regular_16_glyph_107,
|
||||
Font_Unifont_Regular_16_glyph_108,
|
||||
Font_Unifont_Regular_16_glyph_109,
|
||||
Font_Unifont_Regular_16_glyph_110,
|
||||
Font_Unifont_Regular_16_glyph_111,
|
||||
Font_Unifont_Regular_16_glyph_112,
|
||||
Font_Unifont_Regular_16_glyph_113,
|
||||
Font_Unifont_Regular_16_glyph_114,
|
||||
Font_Unifont_Regular_16_glyph_115,
|
||||
Font_Unifont_Regular_16_glyph_116,
|
||||
Font_Unifont_Regular_16_glyph_117,
|
||||
Font_Unifont_Regular_16_glyph_118,
|
||||
Font_Unifont_Regular_16_glyph_119,
|
||||
Font_Unifont_Regular_16_glyph_120,
|
||||
Font_Unifont_Regular_16_glyph_121,
|
||||
Font_Unifont_Regular_16_glyph_122,
|
||||
Font_Unifont_Regular_16_glyph_123,
|
||||
Font_Unifont_Regular_16_glyph_124,
|
||||
Font_Unifont_Regular_16_glyph_125,
|
||||
Font_Unifont_Regular_16_glyph_126,
|
||||
};
|
10
core/embed/lib/fonts/font_unifont_regular_16.h
Normal file
@ -0,0 +1,10 @@
|
||||
#include <stdint.h>
|
||||
|
||||
#if TREZOR_FONT_BPP != 1
|
||||
#error Wrong TREZOR_FONT_BPP (expected 1)
|
||||
#endif
|
||||
#define Font_Unifont_Regular_16_HEIGHT 12 // <--- 12 from 16
|
||||
#define Font_Unifont_Regular_16_MAX_HEIGHT 12 // <--- 12 from 15
|
||||
#define Font_Unifont_Regular_16_BASELINE 2
|
||||
extern const uint8_t* const Font_Unifont_Regular_16[126 + 1 - 32];
|
||||
extern const uint8_t Font_Unifont_Regular_16_glyph_nonprintable[];
|
@ -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;
|
||||
|
BIN
core/embed/rust/src/docs/ButtonsTR.drawio.png
Normal file
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, TextNoOp, TextRenderer},
|
||||
op::OpTextLayout,
|
||||
};
|
||||
|
||||
use super::layout::{
|
||||
LayoutFit, LayoutSink, LineBreaking, Op, PageBreaking, TextLayout, TextRenderer, TextStyle,
|
||||
};
|
||||
|
||||
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
|
||||
fn layout_content(&mut self, sink: &mut dyn LayoutSink) -> LayoutFit {
|
||||
self.op_layout.layout_content(self.char_offset, sink)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_format(mut self, format: F) -> Self {
|
||||
self.format = format;
|
||||
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_font(mut self, text_font: Font) -> Self {
|
||||
self.layout.style.text_font = text_font;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_text_color(mut self, text_color: Color) -> Self {
|
||||
self.layout.style.text_color = text_color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_line_breaking(mut self, line_breaking: LineBreaking) -> Self {
|
||||
self.layout.style.line_breaking = line_breaking;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_page_breaking(mut self, page_breaking: PageBreaking) -> Self {
|
||||
self.layout.style.page_breaking = page_breaking;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_char_offset(&mut self, char_offset: usize) {
|
||||
// Make sure we're starting from the beginning.
|
||||
self.char_offset = char_offset;
|
||||
|
||||
// Looping through the content and counting pages
|
||||
// until we finally fit.
|
||||
loop {
|
||||
let fit = self.layout_content(&mut TextNoOp);
|
||||
match fit {
|
||||
LayoutFit::Fitting { .. } => {
|
||||
break; // TODO: We should consider if there's more content
|
||||
// to render.
|
||||
}
|
||||
LayoutFit::OutOfBounds {
|
||||
processed_chars, ..
|
||||
} => {
|
||||
page_count += 1;
|
||||
char_offset += processed_chars;
|
||||
self.char_offset = char_offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset the char offset back to the beginning.
|
||||
self.char_offset = 0;
|
||||
|
||||
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;
|
||||
|
||||
// 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> 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)
|
||||
}
|
||||
}
|
||||
|
||||
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<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<F, T> crate::trace::Trace for FormattedText<F, T>
|
||||
where
|
||||
F: AsRef<str>,
|
||||
T: AsRef<str>,
|
||||
{
|
||||
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};
|
||||
|
240
core/embed/rust/src/ui/component/text/op.rs
Normal file
@ -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> {
|
||||
|
35
core/embed/rust/src/ui/component/text/util.rs
Normal file
@ -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) };
|
||||
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.")))
|
||||
}
|
||||
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)) {}
|
||||
}
|
||||
|
280
core/embed/rust/src/ui/model_tr/component/address_details.rs
Normal file
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
445
core/embed/rust/src/ui/model_tr/component/button_controller.rs
Normal file
@ -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);
|
||||
}
|
||||
}
|
149
core/embed/rust/src/ui/model_tr/component/changing_text.rs
Normal file
@ -0,0 +1,149 @@
|
||||
use crate::ui::{
|
||||
component::{Component, Event, EventCtx, Never, Pad},
|
||||
display::Font,
|
||||
geometry::{Alignment, Point, Rect},
|
||||
util::long_line_content_with_ellipsis,
|
||||
};
|
||||
|
||||
use super::{common, theme};
|
||||
|
||||
/// Component that allows for "allocating" a standalone line of text anywhere
|
||||
/// on the screen and updating it arbitrarily - without affecting the rest
|
||||
/// and without being affected by other components.
|
||||
pub struct ChangingTextLine<T> {
|
||||
pad: Pad,
|
||||
text: T,
|
||||
font: Font,
|
||||
/// Whether to show the text. Can be disabled.
|
||||
show_content: bool,
|
||||
alignment: Alignment,
|
||||
}
|
||||
|
||||
impl<T> ChangingTextLine<T>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
pub fn new(text: T, font: Font, alignment: Alignment) -> Self {
|
||||
Self {
|
||||
pad: Pad::with_background(theme::BG),
|
||||
text,
|
||||
font,
|
||||
show_content: true,
|
||||
alignment,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn center_mono(text: T) -> Self {
|
||||
Self::new(text, Font::MONO, Alignment::Center)
|
||||
}
|
||||
|
||||
pub fn center_bold(text: T) -> Self {
|
||||
Self::new(text, Font::BOLD, Alignment::Center)
|
||||
}
|
||||
|
||||
/// Update the text to be displayed in the line.
|
||||
pub fn update_text(&mut self, text: T) {
|
||||
self.text = text;
|
||||
}
|
||||
|
||||
/// Get current text.
|
||||
pub fn get_text(&self) -> &T {
|
||||
&self.text
|
||||
}
|
||||
|
||||
/// Whether we should display the text content.
|
||||
/// If not, the whole area (Pad) will still be cleared.
|
||||
/// Is valid until this function is called again.
|
||||
pub fn show_or_not(&mut self, show: bool) {
|
||||
self.show_content = show;
|
||||
}
|
||||
|
||||
/// Gets the height that is needed for this line to fit perfectly
|
||||
/// without affecting the rest of the screen.
|
||||
/// (Accounting for letters that go below the baseline (y, j, ...).)
|
||||
pub fn needed_height(&self) -> i16 {
|
||||
self.font.line_height() + 2
|
||||
}
|
||||
|
||||
/// Y coordinate of text baseline, is the same for all paints.
|
||||
fn y_baseline(&self) -> i16 {
|
||||
self.pad.area.y0 + self.font.line_height()
|
||||
}
|
||||
|
||||
/// Whether the whole text can be painted in the available space
|
||||
fn text_fits_completely(&self) -> bool {
|
||||
self.font.text_width(self.text.as_ref()) <= self.pad.area.width()
|
||||
}
|
||||
|
||||
fn paint_left(&self) {
|
||||
let baseline = Point::new(self.pad.area.x0, self.y_baseline());
|
||||
common::display(baseline, &self.text, self.font);
|
||||
}
|
||||
|
||||
fn paint_center(&self) {
|
||||
let baseline = Point::new(self.pad.area.bottom_center().x, self.y_baseline());
|
||||
common::display_center(baseline, &self.text, self.font);
|
||||
}
|
||||
|
||||
fn paint_right(&self) {
|
||||
let baseline = Point::new(self.pad.area.x1, self.y_baseline());
|
||||
common::display_right(baseline, &self.text, self.font);
|
||||
}
|
||||
|
||||
fn paint_long_content_with_ellipsis(&self) {
|
||||
let text_to_display = long_line_content_with_ellipsis(
|
||||
self.text.as_ref(),
|
||||
"...",
|
||||
self.font,
|
||||
self.pad.area.width(),
|
||||
);
|
||||
|
||||
// Creating the notion of motion by shifting the text left and right with
|
||||
// each new text character.
|
||||
// (So that it is apparent for the user that the text is changing.)
|
||||
let x_offset = if self.text.as_ref().len() % 2 == 0 {
|
||||
0
|
||||
} else {
|
||||
2
|
||||
};
|
||||
|
||||
let baseline = Point::new(self.pad.area.x0 + x_offset, self.y_baseline());
|
||||
common::display(baseline, &text_to_display, self.font);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Component for ChangingTextLine<T>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
type Msg = Never;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.pad.place(bounds);
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
// Always re-painting from scratch.
|
||||
// Effectively clearing the line completely
|
||||
// when `self.show_content` is set to `false`.
|
||||
self.pad.clear();
|
||||
self.pad.paint();
|
||||
if self.show_content {
|
||||
// In the case text cannot fit, show ellipsis and its right part
|
||||
if !self.text_fits_completely() {
|
||||
self.paint_long_content_with_ellipsis();
|
||||
} else {
|
||||
match self.alignment {
|
||||
Alignment::Start => self.paint_left(),
|
||||
Alignment::Center => self.paint_center(),
|
||||
Alignment::End => self.paint_right(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,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);
|
||||
}
|
||||
}
|
28
core/embed/rust/src/ui/model_tr/component/common.rs
Normal file
@ -0,0 +1,28 @@
|
||||
use crate::ui::{
|
||||
display::{self, Font},
|
||||
geometry::Point,
|
||||
};
|
||||
|
||||
use super::theme;
|
||||
|
||||
/// Display white text on black background
|
||||
pub fn display<T: AsRef<str>>(baseline: Point, text: &T, font: Font) {
|
||||
display::text_left(baseline, text.as_ref(), font, theme::FG, theme::BG);
|
||||
}
|
||||
|
||||
/// Display black text on white background
|
||||
pub fn display_inverse<T: AsRef<str>>(baseline: Point, text: &T, font: Font) {
|
||||
display::text_left(baseline, text.as_ref(), font, theme::BG, theme::FG);
|
||||
}
|
||||
|
||||
/// Display white text on black background,
|
||||
/// centered around a baseline Point
|
||||
pub fn display_center<T: AsRef<str>>(baseline: Point, text: &T, font: Font) {
|
||||
display::text_center(baseline, text.as_ref(), font, theme::FG, theme::BG);
|
||||
}
|
||||
|
||||
/// Display white text on black background,
|
||||
/// with right boundary at a baseline Point
|
||||
pub fn display_right<T: AsRef<str>>(baseline: Point, text: &T, font: Font) {
|
||||
display::text_right(baseline, text.as_ref(), font, theme::FG, theme::BG);
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
use crate::{
|
||||
time::Instant,
|
||||
ui::{
|
||||
component::{Component, Event, EventCtx},
|
||||
event::ButtonEvent,
|
||||
geometry::{Point, Rect},
|
||||
model_tr::component::{loader::Loader, ButtonPos, LoaderMsg, LoaderStyleSheet},
|
||||
},
|
||||
};
|
||||
|
||||
pub enum HoldToConfirmMsg {
|
||||
Confirmed,
|
||||
FailedToConfirm,
|
||||
}
|
||||
|
||||
pub struct HoldToConfirm {
|
||||
area: Rect,
|
||||
pos: ButtonPos,
|
||||
loader: Loader,
|
||||
baseline: Point,
|
||||
text_width: i16,
|
||||
}
|
||||
|
||||
impl HoldToConfirm {
|
||||
pub fn new(pos: ButtonPos, text: &'static str, styles: LoaderStyleSheet) -> Self {
|
||||
let text_width = styles.normal.font.text_width(text.as_ref());
|
||||
Self {
|
||||
area: Rect::zero(),
|
||||
pos,
|
||||
loader: Loader::new(text, styles),
|
||||
baseline: Point::zero(),
|
||||
text_width,
|
||||
}
|
||||
}
|
||||
|
||||
fn placement(&mut self, area: Rect, pos: ButtonPos) -> Rect {
|
||||
let button_width = self.text_width + 7;
|
||||
match pos {
|
||||
ButtonPos::Left => area.split_left(button_width).0,
|
||||
ButtonPos::Right => area.split_right(button_width).1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for HoldToConfirm {
|
||||
type Msg = HoldToConfirmMsg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
let loader_area = self.placement(bounds, self.pos);
|
||||
self.loader.place(loader_area)
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
match event {
|
||||
Event::Button(ButtonEvent::ButtonPressed(which)) if self.pos.hit(&which) => {
|
||||
self.loader.start_growing(ctx, Instant::now());
|
||||
}
|
||||
Event::Button(ButtonEvent::ButtonReleased(which)) if self.pos.hit(&which) => {
|
||||
if self.loader.is_animating() {
|
||||
self.loader.start_shrinking(ctx, Instant::now());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
let msg = self.loader.event(ctx, event);
|
||||
|
||||
if let Some(LoaderMsg::GrownCompletely) = msg {
|
||||
return Some(HoldToConfirmMsg::Confirmed);
|
||||
}
|
||||
if let Some(LoaderMsg::ShrunkCompletely) = msg {
|
||||
return Some(HoldToConfirmMsg::FailedToConfirm);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.loader.paint();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for HoldToConfirm {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("HoldToConfirm");
|
||||
t.child("loader", &self.loader);
|
||||
}
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
use super::button::{Button, ButtonMsg::Clicked};
|
||||
use crate::ui::{
|
||||
component::{Child, Component, Event, EventCtx},
|
||||
display::Font,
|
||||
geometry::Rect,
|
||||
};
|
||||
|
||||
pub enum DialogMsg<T> {
|
||||
Content(T),
|
||||
LeftClicked,
|
||||
RightClicked,
|
||||
}
|
||||
|
||||
pub struct Dialog<T, U> {
|
||||
content: Child<T>,
|
||||
left_btn: Option<Child<Button<U>>>,
|
||||
right_btn: Option<Child<Button<U>>>,
|
||||
}
|
||||
|
||||
impl<T, U> Dialog<T, U>
|
||||
where
|
||||
T: Component,
|
||||
U: AsRef<str>,
|
||||
{
|
||||
pub fn new(content: T, left: Option<Button<U>>, right: Option<Button<U>>) -> Self {
|
||||
Self {
|
||||
content: Child::new(content),
|
||||
left_btn: left.map(Child::new),
|
||||
right_btn: right.map(Child::new),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inner(&self) -> &T {
|
||||
self.content.inner()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> Component for Dialog<T, U>
|
||||
where
|
||||
T: Component,
|
||||
U: AsRef<str>,
|
||||
{
|
||||
type Msg = DialogMsg<T::Msg>;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
let button_height = Font::BOLD.line_height() + 2;
|
||||
let (content_area, button_area) = bounds.split_bottom(button_height);
|
||||
self.content.place(content_area);
|
||||
self.left_btn.as_mut().map(|b| b.place(button_area));
|
||||
self.right_btn.as_mut().map(|b| b.place(button_area));
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
if let Some(msg) = self.content.event(ctx, event) {
|
||||
Some(DialogMsg::Content(msg))
|
||||
} else if let Some(Clicked) = self.left_btn.as_mut().and_then(|b| b.event(ctx, event)) {
|
||||
Some(DialogMsg::LeftClicked)
|
||||
} else if let Some(Clicked) = self.right_btn.as_mut().and_then(|b| b.event(ctx, event)) {
|
||||
Some(DialogMsg::RightClicked)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.content.paint();
|
||||
if let Some(b) = self.left_btn.as_mut() {
|
||||
b.paint();
|
||||
}
|
||||
if let Some(b) = self.right_btn.as_mut() {
|
||||
b.paint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T, U> crate::trace::Trace for Dialog<T, U>
|
||||
where
|
||||
T: crate::trace::Trace,
|
||||
U: crate::trace::Trace + AsRef<str>,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("Dialog");
|
||||
t.child("content", &self.content);
|
||||
if let Some(b) = self.left_btn.as_ref() {
|
||||
t.child("left", b)
|
||||
}
|
||||
if let Some(b) = self.right_btn.as_ref() {
|
||||
t.child("right", b)
|
||||
}
|
||||
}
|
||||
}
|
302
core/embed/rust/src/ui/model_tr/component/flow.rs
Normal file
@ -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);
|
||||
}
|
||||
}
|
224
core/embed/rust/src/ui/model_tr/component/flow_pages.rs
Normal file
@ -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,25 +1,130 @@
|
||||
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: StringType + Clone,
|
||||
{
|
||||
type Msg = T::Msg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
const TITLE_SPACE: i16 = 2;
|
||||
|
||||
let (title_area, content_area) = bounds.split_top(theme::FONT_HEADER.line_height());
|
||||
let content_area = content_area.inset(Insets::top(TITLE_SPACE));
|
||||
|
||||
self.title.place(title_area);
|
||||
self.content.place(content_area);
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
self.title.event(ctx, event);
|
||||
self.content.event(ctx, event)
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.title.paint();
|
||||
self.content.paint();
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
@ -27,52 +132,107 @@ where
|
||||
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 Frame<T, U>
|
||||
impl<T, U> Component for ScrollableFrame<T, U>
|
||||
where
|
||||
T: Component,
|
||||
U: AsRef<str>,
|
||||
T: Component + ScrollableContent,
|
||||
U: StringType + Clone,
|
||||
{
|
||||
type Msg = T::Msg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
const TITLE_SPACE: i16 = 4;
|
||||
// Depending whether there is a title or not
|
||||
let (content_area, scrollbar_area, title_area) = if self.title.is_none() {
|
||||
// When the content fits on one page, no need for allocating place for scrollbar
|
||||
self.content.place(bounds);
|
||||
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_area, content_area) = bounds.split_top(Font::BOLD.line_height());
|
||||
let content_area = content_area.inset(Insets::top(TITLE_SPACE));
|
||||
let (title_and_scrollbar_area, content_area) =
|
||||
bounds.split_top(theme::FONT_HEADER.line_height());
|
||||
let content_area = content_area.inset(Insets::top(TITLE_SPACE));
|
||||
|
||||
// When there is only one page, do not allocate anything for scrollbar,
|
||||
// which would reduce the space for title
|
||||
self.content.place(content_area);
|
||||
let 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.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> {
|
||||
self.content.event(ctx, event)
|
||||
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) {
|
||||
display::text_left(
|
||||
self.area.bottom_left() - Offset::y(2),
|
||||
self.title.as_ref(),
|
||||
Font::BOLD,
|
||||
theme::FG,
|
||||
theme::BG,
|
||||
);
|
||||
display::dotted_line(self.area.bottom_left(), self.area.width(), theme::FG);
|
||||
self.title.paint();
|
||||
self.scrollbar.paint();
|
||||
self.content.paint();
|
||||
}
|
||||
}
|
||||
|
||||
// DEBUG-ONLY SECTION BELOW
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T, U> crate::trace::Trace for Frame<T, U>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
140
core/embed/rust/src/ui/model_tr/component/hold_to_confirm.rs
Normal file
@ -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);
|
||||
}
|
||||
}
|
229
core/embed/rust/src/ui/model_tr/component/homescreen.rs
Normal file
@ -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, ¬ification.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());
|
||||
}
|
||||
}
|