feat(all): implement translations into Trezor

Co-authored-by matejcik <ja@matejcik.cz>
pull/3532/head
grdddj 9 months ago committed by Jiří Musil
parent b5c86a45ed
commit b8ea21d24a

@ -46,5 +46,6 @@ include:
- ci/build.yml
- ci/test.yml
- ci/test-hw.yml
- ci/test-nonenglish.yml
- ci/posttest.yml
- ci/deploy.yml

@ -144,6 +144,6 @@ vendorheader: ## generate vendor header
vendorheader_check: ## check that vendor header is up to date
./core/embed/vendorheader/generate.sh --quiet --check
gen: mocks icons templates protobuf ci_docs vendorheader solana_templates ## regenerate auto-generated files from sources
gen: templates mocks icons protobuf ci_docs vendorheader solana ## regenerate auto-generated files from sources
gen_check: mocks_check icons_check templates_check protobuf_check ci_docs_check vendorheader_check solana_templates_check ## check validity of auto-generated files
gen_check: templates_check mocks_check icons_check protobuf_check ci_docs_check vendorheader_check solana_templates_check ## check validity of auto-generated files

@ -261,6 +261,44 @@ ui tests fixtures deploy:
tags:
- deploy
ui tests fixtures deploy nonenglish:
stage: deploy
variables:
DEPLOY_PATH: "${DEPLOY_BASE_DIR}/ui_tests/"
BUCKET: "data.trezor.io"
GIT_SUBMODULE_STRATEGY: "none"
only:
- schedules # nightly build
- /translations/ # translations branches
before_script: [] # no poetry
interruptible: false
needs:
- core click test czech
- core device test czech
- core device R test czech
- core click R test czech
- core click test german
- core device test german
- core device R test german
- core click R test german
- core click test french
- core device test french
- core device R test french
- core click R test french
- core click test spanish
- core device test spanish
- core device R test spanish
- core click R test spanish
script:
- echo "Deploying to $DEPLOY_PATH"
- rsync --delete -va ci/ui_test_records/* "$DEPLOY_PATH"
- source ${AWS_DEPLOY_DATA}
- aws s3 sync $DEPLOY_PATH s3://$BUCKET/dev/firmware/ui_tests
# This "hack" is needed because aws does not have an easy option to generate autoindex. We fetch the one autogenerated by nginx on local server.
- wget https://firmware.corp.sldev.cz/ui_tests/ -O index.html && aws s3 cp index.html s3://$BUCKET/dev/firmware/ui_tests/
tags:
- deploy
# sync to aws
sync emulators to aws:

@ -0,0 +1,166 @@
from __future__ import annotations
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Callable
HERE = Path(__file__).resolve().parent
TEST_FILE = HERE / "test-nonenglish.yml"
LANGS = {
"cs": "czech",
"fr": "french",
"de": "german",
"es": "spanish",
}
MODELS = ["T", "R"]
@dataclass
class Replacement:
start: str
end: str
replacement: str
def replace_content_between_markers(
file_path: Path | str, replacements: list[Replacement]
) -> None:
with open(file_path, "r") as file:
content = file.read()
for replace in replacements:
pattern = rf"({replace.start}.*?{replace.end})"
content = re.sub(
pattern,
f"{replace.start}\n{replace.replacement}\n{replace.end}",
content,
flags=re.DOTALL,
)
with open(file_path, "w") as file:
file.write(content)
def get_device_test(lang: str, model: str) -> str:
lang_long = LANGS[lang]
model_or_empty = f" {model}" if model != "T" else ""
model_needs_or_empty = f" {model}" if model != "T" else ""
return f"""\
core device{model_or_empty} test {lang_long}:
stage: test
<<: *gitlab_caching
needs:
- core unix frozen{model_needs_or_empty} debug build
variables:
TREZOR_PROFILING: "1" # so that we get coverage data
TREZOR_MODEL: "{model}"
MULTICORE: "4" # more could interfere with other jobs
TEST_LANG: "{lang}" # {lang_long}
only:
- schedules # nightly build
- /translations/ # translations branches
script:
- $NIX_SHELL --run "poetry run make -C core test_emu_ui_multicore | ts -s"
after_script:
- mv core/src/.coverage.* core # there will be more coverage files (one per core)
- 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/ui_tests/fixtures.results.json
- tests/junit.xml
- tests/trezor.log
- core/.coverage.*
when: always
expire_in: 1 week
reports:
junit: tests/junit.xml
"""
def get_click_test(lang: str, model: str) -> str:
lang_long = LANGS[lang]
model_or_empty = f" {model}" if model != "T" else ""
model_needs_or_empty = f" {model}" if model != "T" else ""
return f"""\
core click{model_or_empty} test {lang_long}:
stage: test
<<: *gitlab_caching
needs:
- core unix frozen{model_needs_or_empty} debug build
variables:
TREZOR_PROFILING: "1" # so that we get coverage data
TREZOR_MODEL: "{model}"
TEST_LANG: "{lang}" # {lang_long}
only:
- schedules # nightly build
- /translations/ # translations branches
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/ui_tests/fixtures.results.json
- tests/trezor.log
- tests/junit.xml
- core/.coverage.*
reports:
junit: tests/junit.xml
expire_in: 1 week
when: always
"""
def get_all_tests_text(func: Callable[[str, str], str]) -> str:
text = ""
for model in MODELS:
for lang in LANGS:
content = func(lang, model)
text += content + "\n"
return text
def fill_device_tests() -> None:
replacement = Replacement(
start=r"##START_DEVICE_TESTS",
end=r"##END_DEVICE_TESTS",
replacement=get_all_tests_text(get_device_test),
)
replace_content_between_markers(TEST_FILE, [replacement])
def fill_click_tests() -> None:
replacement = Replacement(
start=r"##START_CLICK_TESTS",
end=r"##END_CLICK_TESTS",
replacement=get_all_tests_text(get_click_test),
)
replace_content_between_markers(TEST_FILE, [replacement])
if __name__ == "__main__":
fill_device_tests()
fill_click_tests()

@ -97,6 +97,7 @@ stdenvNoCC.mkDerivation ({
bash
bloaty # for binsize
check
crowdin-cli # for translations
curl # for connect tests
editorconfig-checker
gcc-arm-embedded

@ -0,0 +1,587 @@
# Tests for non-english languages, that run only nightly
# - apart from that, they run also for every branch containing "translations" in its name
image: registry.gitlab.com/satoshilabs/trezor/trezor-firmware/trezor-firmware-env.nix
# Caching
.gitlab_caching: &gitlab_caching
cache:
key: "$CI_COMMIT_REF_SLUG"
paths:
- .venv/
##START_DEVICE_TESTS
core device test czech:
stage: test
<<: *gitlab_caching
needs:
- core unix frozen debug build
variables:
TREZOR_PROFILING: "1" # so that we get coverage data
TREZOR_MODEL: "T"
MULTICORE: "4" # more could interfere with other jobs
TEST_LANG: "cs" # czech
only:
- schedules # nightly build
- /translations/ # translations branches
script:
- $NIX_SHELL --run "poetry run make -C core test_emu_ui_multicore | ts -s"
after_script:
- mv core/src/.coverage.* core # there will be more coverage files (one per core)
- 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/ui_tests/fixtures.results.json
- tests/junit.xml
- tests/trezor.log
- core/.coverage.*
when: always
expire_in: 1 week
reports:
junit: tests/junit.xml
core device test french:
stage: test
<<: *gitlab_caching
needs:
- core unix frozen debug build
variables:
TREZOR_PROFILING: "1" # so that we get coverage data
TREZOR_MODEL: "T"
MULTICORE: "4" # more could interfere with other jobs
TEST_LANG: "fr" # french
only:
- schedules # nightly build
- /translations/ # translations branches
script:
- $NIX_SHELL --run "poetry run make -C core test_emu_ui_multicore | ts -s"
after_script:
- mv core/src/.coverage.* core # there will be more coverage files (one per core)
- 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/ui_tests/fixtures.results.json
- tests/junit.xml
- tests/trezor.log
- core/.coverage.*
when: always
expire_in: 1 week
reports:
junit: tests/junit.xml
core device test german:
stage: test
<<: *gitlab_caching
needs:
- core unix frozen debug build
variables:
TREZOR_PROFILING: "1" # so that we get coverage data
TREZOR_MODEL: "T"
MULTICORE: "4" # more could interfere with other jobs
TEST_LANG: "de" # german
only:
- schedules # nightly build
- /translations/ # translations branches
script:
- $NIX_SHELL --run "poetry run make -C core test_emu_ui_multicore | ts -s"
after_script:
- mv core/src/.coverage.* core # there will be more coverage files (one per core)
- 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/ui_tests/fixtures.results.json
- tests/junit.xml
- tests/trezor.log
- core/.coverage.*
when: always
expire_in: 1 week
reports:
junit: tests/junit.xml
core device test spanish:
stage: test
<<: *gitlab_caching
needs:
- core unix frozen debug build
variables:
TREZOR_PROFILING: "1" # so that we get coverage data
TREZOR_MODEL: "T"
MULTICORE: "4" # more could interfere with other jobs
TEST_LANG: "es" # spanish
only:
- schedules # nightly build
- /translations/ # translations branches
script:
- $NIX_SHELL --run "poetry run make -C core test_emu_ui_multicore | ts -s"
after_script:
- mv core/src/.coverage.* core # there will be more coverage files (one per core)
- 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/ui_tests/fixtures.results.json
- tests/junit.xml
- tests/trezor.log
- core/.coverage.*
when: always
expire_in: 1 week
reports:
junit: tests/junit.xml
core device R test czech:
stage: test
<<: *gitlab_caching
needs:
- core unix frozen R debug build
variables:
TREZOR_PROFILING: "1" # so that we get coverage data
TREZOR_MODEL: "R"
MULTICORE: "4" # more could interfere with other jobs
TEST_LANG: "cs" # czech
only:
- schedules # nightly build
- /translations/ # translations branches
script:
- $NIX_SHELL --run "poetry run make -C core test_emu_ui_multicore | ts -s"
after_script:
- mv core/src/.coverage.* core # there will be more coverage files (one per core)
- 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/ui_tests/fixtures.results.json
- tests/junit.xml
- tests/trezor.log
- core/.coverage.*
when: always
expire_in: 1 week
reports:
junit: tests/junit.xml
core device R test french:
stage: test
<<: *gitlab_caching
needs:
- core unix frozen R debug build
variables:
TREZOR_PROFILING: "1" # so that we get coverage data
TREZOR_MODEL: "R"
MULTICORE: "4" # more could interfere with other jobs
TEST_LANG: "fr" # french
only:
- schedules # nightly build
- /translations/ # translations branches
script:
- $NIX_SHELL --run "poetry run make -C core test_emu_ui_multicore | ts -s"
after_script:
- mv core/src/.coverage.* core # there will be more coverage files (one per core)
- 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/ui_tests/fixtures.results.json
- tests/junit.xml
- tests/trezor.log
- core/.coverage.*
when: always
expire_in: 1 week
reports:
junit: tests/junit.xml
core device R test german:
stage: test
<<: *gitlab_caching
needs:
- core unix frozen R debug build
variables:
TREZOR_PROFILING: "1" # so that we get coverage data
TREZOR_MODEL: "R"
MULTICORE: "4" # more could interfere with other jobs
TEST_LANG: "de" # german
only:
- schedules # nightly build
- /translations/ # translations branches
script:
- $NIX_SHELL --run "poetry run make -C core test_emu_ui_multicore | ts -s"
after_script:
- mv core/src/.coverage.* core # there will be more coverage files (one per core)
- 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/ui_tests/fixtures.results.json
- tests/junit.xml
- tests/trezor.log
- core/.coverage.*
when: always
expire_in: 1 week
reports:
junit: tests/junit.xml
core device R test spanish:
stage: test
<<: *gitlab_caching
needs:
- core unix frozen R debug build
variables:
TREZOR_PROFILING: "1" # so that we get coverage data
TREZOR_MODEL: "R"
MULTICORE: "4" # more could interfere with other jobs
TEST_LANG: "es" # spanish
only:
- schedules # nightly build
- /translations/ # translations branches
script:
- $NIX_SHELL --run "poetry run make -C core test_emu_ui_multicore | ts -s"
after_script:
- mv core/src/.coverage.* core # there will be more coverage files (one per core)
- 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/ui_tests/fixtures.results.json
- tests/junit.xml
- tests/trezor.log
- core/.coverage.*
when: always
expire_in: 1 week
reports:
junit: tests/junit.xml
##END_DEVICE_TESTS
##START_CLICK_TESTS
core click test czech:
stage: test
<<: *gitlab_caching
needs:
- core unix frozen debug build
variables:
TREZOR_PROFILING: "1" # so that we get coverage data
TREZOR_MODEL: "T"
TEST_LANG: "cs" # czech
only:
- schedules # nightly build
- /translations/ # translations branches
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/ui_tests/fixtures.results.json
- tests/trezor.log
- tests/junit.xml
- core/.coverage.*
reports:
junit: tests/junit.xml
expire_in: 1 week
when: always
core click test french:
stage: test
<<: *gitlab_caching
needs:
- core unix frozen debug build
variables:
TREZOR_PROFILING: "1" # so that we get coverage data
TREZOR_MODEL: "T"
TEST_LANG: "fr" # french
only:
- schedules # nightly build
- /translations/ # translations branches
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/ui_tests/fixtures.results.json
- tests/trezor.log
- tests/junit.xml
- core/.coverage.*
reports:
junit: tests/junit.xml
expire_in: 1 week
when: always
core click test german:
stage: test
<<: *gitlab_caching
needs:
- core unix frozen debug build
variables:
TREZOR_PROFILING: "1" # so that we get coverage data
TREZOR_MODEL: "T"
TEST_LANG: "de" # german
only:
- schedules # nightly build
- /translations/ # translations branches
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/ui_tests/fixtures.results.json
- tests/trezor.log
- tests/junit.xml
- core/.coverage.*
reports:
junit: tests/junit.xml
expire_in: 1 week
when: always
core click test spanish:
stage: test
<<: *gitlab_caching
needs:
- core unix frozen debug build
variables:
TREZOR_PROFILING: "1" # so that we get coverage data
TREZOR_MODEL: "T"
TEST_LANG: "es" # spanish
only:
- schedules # nightly build
- /translations/ # translations branches
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/ui_tests/fixtures.results.json
- tests/trezor.log
- tests/junit.xml
- core/.coverage.*
reports:
junit: tests/junit.xml
expire_in: 1 week
when: always
core click R test czech:
stage: test
<<: *gitlab_caching
needs:
- core unix frozen R debug build
variables:
TREZOR_PROFILING: "1" # so that we get coverage data
TREZOR_MODEL: "R"
TEST_LANG: "cs" # czech
only:
- schedules # nightly build
- /translations/ # translations branches
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/ui_tests/fixtures.results.json
- tests/trezor.log
- tests/junit.xml
- core/.coverage.*
reports:
junit: tests/junit.xml
expire_in: 1 week
when: always
core click R test french:
stage: test
<<: *gitlab_caching
needs:
- core unix frozen R debug build
variables:
TREZOR_PROFILING: "1" # so that we get coverage data
TREZOR_MODEL: "R"
TEST_LANG: "fr" # french
only:
- schedules # nightly build
- /translations/ # translations branches
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/ui_tests/fixtures.results.json
- tests/trezor.log
- tests/junit.xml
- core/.coverage.*
reports:
junit: tests/junit.xml
expire_in: 1 week
when: always
core click R test german:
stage: test
<<: *gitlab_caching
needs:
- core unix frozen R debug build
variables:
TREZOR_PROFILING: "1" # so that we get coverage data
TREZOR_MODEL: "R"
TEST_LANG: "de" # german
only:
- schedules # nightly build
- /translations/ # translations branches
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/ui_tests/fixtures.results.json
- tests/trezor.log
- tests/junit.xml
- core/.coverage.*
reports:
junit: tests/junit.xml
expire_in: 1 week
when: always
core click R test spanish:
stage: test
<<: *gitlab_caching
needs:
- core unix frozen R debug build
variables:
TREZOR_PROFILING: "1" # so that we get coverage data
TREZOR_MODEL: "R"
TEST_LANG: "es" # spanish
only:
- schedules # nightly build
- /translations/ # translations branches
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/ui_tests/fixtures.results.json
- tests/trezor.log
- tests/junit.xml
- core/.coverage.*
reports:
junit: tests/junit.xml
expire_in: 1 week
when: always
##END_CLICK_TESTS

@ -161,7 +161,7 @@ message EndSession {
}
/**
* Request: change language and/or label of the device
* Request: change some property of the device, e.g. label or homescreen
* @start
* @next Success
* @next Failure
@ -180,6 +180,47 @@ message ApplySettings {
optional bool hide_passphrase_from_host = 11; // do not show passphrase coming from host
}
/**
* Request: change the device language via translation data.
* Does not send the translation data itself, as they are too large for one message.
* Device will request the translation data in chunks.
* @start
* @next TranslationDataRequest
* @next Failure
*/
message ChangeLanguage {
// byte length of the whole translation blob (set to 0 for default language - english)
required uint32 data_length = 1;
// Prompt the user on screen.
// In certain conditions (such as freshly installed device), the confirmation prompt
// is not mandatory. Setting show_display=false will skip the prompt if that's
// the case. If the device does not allow skipping the prompt, a request with
// show_display=false will return a failure. (This way the host can safely try
// to change the language without invoking a prompt.)
// Setting show_display to true will always show the prompt.
// Leaving the option unset will show the prompt only when necessary.
optional bool show_display = 2;
}
/**
* Response: Device asks for more data from transaction payload.
* @end
* @next TranslationDataAck
*/
message TranslationDataRequest {
required uint32 data_length = 1; // Number of bytes being requested
required uint32 data_offset = 2; // Offset of the first byte being requested
}
/**
* Request: Translation payload data.
* @next TranslationDataRequest
* @next Success
*/
message TranslationDataAck {
required bytes data_chunk = 1; // Bytes from translation payload
}
/**
* Request: set flags of the device
* @start
@ -487,11 +528,17 @@ message CancelAuthorization {
* @next Success
*/
message RebootToBootloader {
// Action to be performed after rebooting to bootloader
optional BootCommand boot_command = 1 [default=STOP_AND_WAIT];
// Firmware header to be flashed after rebooting to bootloader
optional bytes firmware_header = 2;
// Length of language blob to be installed before upgrading firmware
optional uint32 language_data_length = 3 [default=0];
enum BootCommand {
// Go to bootloader menu
STOP_AND_WAIT = 0;
// Connect to host and wait for firmware update
INSTALL_UPGRADE = 1;
}
}

@ -124,6 +124,9 @@ enum MessageType {
MessageType_UnlockBootloader = 96 [(bitcoin_only) = true, (wire_in) = true];
MessageType_AuthenticateDevice = 97 [(bitcoin_only) = true, (wire_out) = true];
MessageType_AuthenticityProof = 98 [(bitcoin_only) = true, (wire_in) = true];
MessageType_ChangeLanguage = 990 [(bitcoin_only) = true, (wire_in) = true];
MessageType_TranslationDataRequest = 991 [(bitcoin_only) = true, (wire_out) = true];
MessageType_TranslationDataAck = 992 [(bitcoin_only) = true, (wire_in) = true];
MessageType_SetU2FCounter = 63 [(wire_in) = true];
MessageType_GetNextU2FCounter = 80 [(wire_in) = true];

@ -116,6 +116,10 @@ def ascii_filter(s: str) -> str:
return re.sub("[^ -\x7e]", "_", s)
def utf8_str_filter(s: str) -> str:
return '"' + repr(s)[1:-1] + '"'
def make_support_filter(
support_info: SupportInfo,
) -> Callable[[str, Coins], Iterator[Coin]]:
@ -126,6 +130,7 @@ def make_support_filter(
MAKO_FILTERS = {
"utf8_str": utf8_str_filter,
"c_str": c_str_filter,
"ascii": ascii_filter,
"black_repr": black_repr_filter,

@ -34,7 +34,8 @@ TREZOR_MODEL ?= T
TREZOR_MEMPERF ?= 0
ADDRESS_SANITIZER ?= 0
CMAKELISTS ?= 0
PYTEST_TIMEOUT ?= 400
PYTEST_TIMEOUT ?= 500
TEST_LANG ?= "en"
# OpenOCD interface default. Alternative: ftdi/olimex-arm-usb-tiny-h
OPENOCD_INTERFACE ?= stlink
@ -101,11 +102,12 @@ test_rust: ## run rs unit tests
-- --test-threads=1 --nocapture
test_emu: ## run selected device tests from python-trezor
$(EMU_TEST) $(PYTEST) $(TESTPATH)/device_tests $(TESTOPTS)
$(EMU_TEST) $(PYTEST) $(TESTPATH)/device_tests $(TESTOPTS) --lang=$(TEST_LANG)
test_emu_multicore: ## run device tests using multiple cores
$(PYTEST) -n $(MULTICORE) $(TESTPATH)/device_tests $(TESTOPTS) --timeout $(PYTEST_TIMEOUT) \
--control-emulators --model=core --random-order-seed=$(shell echo $$RANDOM)
--control-emulators --model=core --random-order-seed=$(shell echo $$RANDOM) \
--lang=$(TEST_LANG)
test_emu_monero: ## run selected monero device tests from monero-agent
cd tests ; $(EMU_TEST) ./run_tests_device_emu_monero.sh $(TESTOPTS)
@ -119,31 +121,33 @@ test_emu_fido2: ## run fido2 device tests
$(EMU_TEST) --slip0014 $(PYTEST) --maxfail=5 --sim tests/standard/ --vendor trezor $(TESTOPTS)
test_emu_click: ## run click tests
$(EMU_TEST) $(PYTEST) $(TESTPATH)/click_tests $(TESTOPTS)
$(EMU_TEST) $(PYTEST) $(TESTPATH)/click_tests $(TESTOPTS) --lang=$(TEST_LANG)
test_emu_click_ui: ## run click tests with UI testing
$(EMU_TEST) $(PYTEST) $(TESTPATH)/click_tests $(TESTOPTS) \
--ui=test --ui-check-missing --do-master-diff
--ui=test --ui-check-missing --do-master-diff --lang=$(TEST_LANG)
test_emu_persistence: ## run persistence tests
$(PYTEST) $(TESTPATH)/persistence_tests $(TESTOPTS)
$(PYTEST) $(TESTPATH)/persistence_tests $(TESTOPTS) --lang=$(TEST_LANG)
test_emu_persistence_ui: ## run persistence tests with UI testing
$(PYTEST) $(TESTPATH)/persistence_tests $(TESTOPTS) \
--ui=test --ui-check-missing --do-master-diff
--ui=test --ui-check-missing --do-master-diff --lang=$(TEST_LANG)
test_emu_ui: ## run ui integration tests
$(EMU_TEST) $(PYTEST) $(TESTPATH)/device_tests $(TESTOPTS) \
--ui=test --ui-check-missing --record-text-layout --do-master-diff
--ui=test --ui-check-missing --record-text-layout --do-master-diff \
--lang=$(TEST_LANG)
test_emu_ui_multicore: ## run ui integration tests using multiple cores
$(PYTEST) -n $(MULTICORE) $(TESTPATH)/device_tests $(TESTOPTS) --timeout $(PYTEST_TIMEOUT) \
--ui=test --ui-check-missing --record-text-layout --do-master-diff \
--control-emulators --model=core --random-order-seed=$(shell echo $$RANDOM)
--control-emulators --model=core --random-order-seed=$(shell echo $$RANDOM) \
--lang=$(TEST_LANG)
test_emu_ui_record: ## record and hash screens for ui integration tests
$(EMU_TEST) $(PYTEST) $(TESTPATH)/device_tests $(TESTOPTS) \
--ui=record --ui-check-missing --do-master-diff
--ui=record --ui-check-missing --do-master-diff --lang=$(TEST_LANG)
test_emu_ui_record_multicore: ## quickly record all screens
make test_emu_ui_multicore || echo "All errors are recorded in fixtures.json"

@ -204,6 +204,7 @@ SOURCE_MOD += [
CPPDEFINES_MOD += [
'TREZOR_UI2',
'TRANSLATIONS',
]
if TREZOR_MODEL not in ('1', ):
@ -717,6 +718,7 @@ def cargo_build():
if BITCOIN_ONLY == '1':
features.append('bitcoin_only')
features.append('ui')
features.append('translations')
if PYOPT == '0':
features.append('ui_debug')

@ -219,6 +219,7 @@ elif TREZOR_MODEL in ('R', ):
CPPDEFINES_MOD += [
'TREZOR_UI2',
'TRANSLATIONS',
]
if TREZOR_MODEL not in ('1', ):
CPPDEFINES_MOD += [
@ -372,6 +373,7 @@ SOURCE_MICROPYTHON = [
]
SOURCE_UNIX = [
'embed/trezorhal/unix/translations.c',
'embed/trezorhal/unix/common.c',
'embed/trezorhal/unix/display-unix.c',
'embed/trezorhal/unix/flash.c',
@ -795,6 +797,7 @@ def cargo_build():
if BITCOIN_ONLY == '1':
features.append('bitcoin_only')
features.append('ui')
features.append('translations')
if PYOPT == '0':
features.append('debug')
if DMA2D:

@ -295,17 +295,18 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(
mod_trezorutils_reboot_to_bootloader_obj, 0, 2,
mod_trezorutils_reboot_to_bootloader);
/// def check_firmware_header(
/// header : bytes
/// ) -> dict:
/// """
/// Checks firmware image and vendor header and returns
/// { "version": (major, minor, patch),
/// "vendor": string,
/// "fingerprint": bytes,
/// "hash": bytes
/// }
/// """
/// VersionTuple = Tuple[int, int, int, int]
///
/// class FirmwareHeaderInfo(NamedTuple):
/// version: VersionTuple
/// vendor: str
/// fingerprint: bytes
/// hash: bytes
///
/// mock:global
///
/// def check_firmware_header(header : bytes) -> FirmwareHeaderInfo:
/// """Parses incoming firmware header and returns information about it."""
STATIC mp_obj_t mod_trezorutils_check_firmware_header(mp_obj_t header) {
mp_buffer_info_t header_buf = {0};
mp_get_buffer_raise(header, &header_buf, MP_BUFFER_READ);
@ -313,23 +314,18 @@ STATIC mp_obj_t mod_trezorutils_check_firmware_header(mp_obj_t header) {
firmware_header_info_t info;
if (sectrue == check_firmware_header(header_buf.buf, header_buf.len, &info)) {
mp_obj_t version[3] = {mp_obj_new_int(info.ver_major),
mp_obj_new_int(info.ver_minor),
mp_obj_new_int(info.ver_patch)};
mp_obj_t result = mp_obj_new_dict(4);
mp_obj_dict_store(result, MP_ROM_QSTR(MP_QSTR_version),
mp_obj_new_tuple(MP_ARRAY_SIZE(version), version));
mp_obj_dict_store(
result, MP_ROM_QSTR(MP_QSTR_vendor),
mp_obj_new_str_copy(&mp_type_str, info.vstr, info.vstr_len));
mp_obj_dict_store(
result, MP_ROM_QSTR(MP_QSTR_fingerprint),
mp_obj_new_bytes(info.fingerprint, sizeof(info.fingerprint)));
mp_obj_dict_store(result, MP_ROM_QSTR(MP_QSTR_hash),
mp_obj_new_bytes(info.hash, sizeof(info.hash)));
return result;
mp_obj_t version[4] = {
mp_obj_new_int(info.ver_major), mp_obj_new_int(info.ver_minor),
mp_obj_new_int(info.ver_patch), mp_obj_new_int(info.ver_build)};
static const qstr fields[4] = {MP_QSTR_version, MP_QSTR_vendor,
MP_QSTR_fingerprint, MP_QSTR_hash};
const mp_obj_t values[4] = {
mp_obj_new_tuple(MP_ARRAY_SIZE(version), version),
mp_obj_new_str_copy(&mp_type_str, info.vstr, info.vstr_len),
mp_obj_new_bytes(info.fingerprint, sizeof(info.fingerprint)),
mp_obj_new_bytes(info.hash, sizeof(info.hash))};
return mp_obj_new_attrtuple(fields, MP_ARRAY_SIZE(fields), values);
}
mp_raise_ValueError("Invalid value.");
@ -370,14 +366,16 @@ STATIC mp_obj_str_t mod_trezorutils_full_name_obj = {
sizeof(MODEL_FULL_NAME) - 1,
(const byte *)MODEL_FULL_NAME};
STATIC mp_obj_tuple_t mod_trezorutils_version_obj = {
{&mp_type_tuple},
4,
{MP_OBJ_NEW_SMALL_INT(VERSION_MAJOR), MP_OBJ_NEW_SMALL_INT(VERSION_MINOR),
MP_OBJ_NEW_SMALL_INT(VERSION_PATCH), MP_OBJ_NEW_SMALL_INT(VERSION_BUILD)}};
/// SCM_REVISION: bytes
/// """Git commit hash of the firmware."""
/// VERSION_MAJOR: int
/// """Major version."""
/// VERSION_MINOR: int
/// """Minor version."""
/// VERSION_PATCH: int
/// """Patch version."""
/// VERSION: VersionTuple
/// """Firmware version as a tuple (major, minor, patch, build)."""
/// USE_SD_CARD: bool
/// """Whether the hardware supports SD card."""
/// USE_BACKLIGHT: bool
@ -419,9 +417,7 @@ STATIC const mp_rom_map_elem_t mp_module_trezorutils_globals_table[] = {
// various built-in constants
{MP_ROM_QSTR(MP_QSTR_SCM_REVISION),
MP_ROM_PTR(&mod_trezorutils_revision_obj)},
{MP_ROM_QSTR(MP_QSTR_VERSION_MAJOR), MP_ROM_INT(VERSION_MAJOR)},
{MP_ROM_QSTR(MP_QSTR_VERSION_MINOR), MP_ROM_INT(VERSION_MINOR)},
{MP_ROM_QSTR(MP_QSTR_VERSION_PATCH), MP_ROM_INT(VERSION_PATCH)},
{MP_ROM_QSTR(MP_QSTR_VERSION), MP_ROM_PTR(&mod_trezorutils_version_obj)},
#ifdef USE_SD_CARD
{MP_ROM_QSTR(MP_QSTR_USE_SD_CARD), mp_const_true},
#else

@ -18,6 +18,7 @@
*/
#include "librust.h"
#include "librust_fonts.h"
#include "py/runtime.h"
#if MICROPY_PY_TREZORUI2
@ -27,3 +28,7 @@ MP_REGISTER_MODULE(MP_QSTR_trezorui2, mp_module_trezorui2);
#if MICROPY_PY_TREZORPROTO
MP_REGISTER_MODULE(MP_QSTR_trezorproto, mp_module_trezorproto);
#endif
#if MICROPY_PY_TREZORTRANSLATE
MP_REGISTER_MODULE(MP_QSTR_trezortranslate, mp_module_trezortranslate);
#endif

@ -163,6 +163,7 @@
#define MICROPY_PY_TREZORUI (1)
#define MICROPY_PY_TREZORUTILS (1)
#define MICROPY_PY_TREZORPROTO (1)
#define MICROPY_PY_TREZORTRANSLATE (1)
#define MICROPY_PY_TREZORUI2 (1)
#ifdef SYSTEM_VIEW

@ -99,9 +99,9 @@ void display_text_render_buffer(const char *text, int textlen, int font,
int baseline = font_baseline(font);
// render glyphs
for (int c_idx = 0; c_idx < textlen; c_idx++) {
const uint8_t *g = font_get_glyph(font, (uint8_t)text[c_idx]);
if (!g) continue;
font_glyph_iter_t iter = font_glyph_iter_init(font, (uint8_t *)text, textlen);
const uint8_t *g = NULL;
while (font_next_glyph(&iter, &g)) {
const uint8_t w = g[0]; // width
const uint8_t h = g[1]; // height
const uint8_t adv = g[2]; // advance
@ -171,9 +171,9 @@ static void display_text_render(int x, int y, const char *text, int textlen,
set_color_table(colortable, fgcolor, bgcolor);
// render glyphs
for (int c_idx = 0; c_idx < textlen; c_idx++) {
const uint8_t *g = font_get_glyph(font, (uint8_t)text[c_idx]);
if (!g) continue;
font_glyph_iter_t iter = font_glyph_iter_init(font, (uint8_t *)text, textlen);
const uint8_t *g = NULL;
while (font_next_glyph(&iter, &g)) {
const uint8_t w = g[0]; // width
const uint8_t h = g[1]; // height
const uint8_t adv = g[2]; // advance
@ -234,9 +234,9 @@ static void display_text_render(int x, int y, const char *text, int textlen,
set_color_table(colortable, fgcolor, bgcolor);
// render glyphs
for (int i = 0; i < textlen; i++) {
const uint8_t *g = font_get_glyph(font, (uint8_t)text[i]);
if (!g) continue;
font_glyph_iter_t iter = font_glyph_iter_init(font, (uint8_t *)text, textlen);
const uint8_t *g = NULL;
while (font_next_glyph(&iter, &g)) {
const uint8_t w = g[0]; // width
const uint8_t h = g[1]; // height
const uint8_t adv = g[2]; // advance

@ -102,7 +102,7 @@
/* } */ static const uint8_t Font_PixelOperator_Bold_8_glyph_125[] = { 5, 7, 6, 0, 7, 225, 140, 51, 27, 128 };
/* ~ */ static const uint8_t Font_PixelOperator_Bold_8_glyph_126[] = { 7, 2, 8, 0, 7, 119, 184 };
const uint8_t Font_PixelOperator_Bold_8_glyph_nonprintable[] = { 6, 7, 7, 0, 7, 132, 207, 57, 207, 252, 255 };
/* ? */ const uint8_t Font_PixelOperator_Bold_8_glyph_nonprintable[] = { 6, 7, 7, 0, 7, 132, 207, 57, 207, 252, 255 };
const uint8_t * const Font_PixelOperator_Bold_8[126 + 1 - 32] = {
Font_PixelOperator_Bold_8_glyph_32,

@ -102,7 +102,7 @@
/* } */ static const uint8_t Font_PixelOperator_Regular_8_glyph_125[] = { 4, 7, 6, 0, 7, 194, 33, 34, 192 };
/* ~ */ static const uint8_t Font_PixelOperator_Regular_8_glyph_126[] = { 6, 2, 7, 0, 7, 102, 96 };
const uint8_t Font_PixelOperator_Regular_8_glyph_nonprintable[] = { 5, 7, 6, 0, 7, 139, 189, 221, 255, 127 };
/* ? */ const uint8_t Font_PixelOperator_Regular_8_glyph_nonprintable[] = { 5, 7, 6, 0, 7, 139, 189, 221, 255, 127 };
const uint8_t * const Font_PixelOperator_Regular_8[126 + 1 - 32] = {
Font_PixelOperator_Regular_8_glyph_32,

@ -105,7 +105,7 @@
/* } */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_125[] = { 4, 7, 7, 0, 7, 194, 33, 34, 192 };
/* ~ */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_126[] = { 6, 2, 7, 0, 7, 102, 96 };
const uint8_t Font_PixelOperatorMono_Regular_8_glyph_nonprintable[] = { 5, 7, 7, 0, 7, 139, 189, 221, 255, 127 };
/* ? */ const uint8_t Font_PixelOperatorMono_Regular_8_glyph_nonprintable[] = { 5, 7, 7, 0, 7, 139, 189, 221, 255, 127 };
const uint8_t * const Font_PixelOperatorMono_Regular_8[126 + 1 - 32] = {
Font_PixelOperatorMono_Regular_8_glyph_32,

@ -102,7 +102,7 @@
/* } */ static const uint8_t Font_Roboto_Bold_20_glyph_125[] = { 7, 20, 7, 0, 16, 180, 3, 0, 0, 248, 79, 0, 0, 176, 239, 0, 0, 64, 255, 3, 0, 32, 255, 5, 0, 32, 255, 6, 0, 32, 255, 6, 0, 16, 255, 7, 0, 0, 252, 45, 0, 0, 210, 255, 2, 0, 210, 255, 2, 0, 252, 45, 0, 16, 255, 7, 0, 32, 255, 6, 0, 32, 255, 6, 0, 32, 255, 5, 0, 64, 255, 3, 0, 176, 239, 0, 0, 248, 79, 0, 0, 180, 3, 0, 0 };
/* ~ */ static const uint8_t Font_Roboto_Bold_20_glyph_126[] = { 11, 5, 13, 1, 8, 96, 254, 60, 0, 162, 9, 245, 255, 255, 6, 249, 12, 252, 108, 252, 255, 255, 6, 237, 3, 128, 255, 175, 0, 0, 0, 0, 65, 2, 0 };
const uint8_t Font_Roboto_Bold_20_glyph_nonprintable[] = { 10, 14, 10, 0, 14, 255, 40, 16, 163, 255, 79, 0, 0, 0, 247, 11, 16, 137, 0, 240, 8, 160, 255, 5, 192, 255, 255, 255, 4, 208, 255, 255, 175, 0, 243, 255, 255, 10, 16, 253, 255, 239, 0, 209, 255, 255, 143, 0, 251, 255, 255, 111, 16, 255, 255, 255, 255, 255, 255, 255, 255, 159, 65, 255, 255, 255, 47, 0, 252, 255, 255, 143, 64, 255, 255 };
/* ? */ const uint8_t Font_Roboto_Bold_20_glyph_nonprintable[] = { 10, 14, 10, 0, 14, 255, 40, 16, 163, 255, 79, 0, 0, 0, 247, 11, 16, 137, 0, 240, 8, 160, 255, 5, 192, 255, 255, 255, 4, 208, 255, 255, 175, 0, 243, 255, 255, 10, 16, 253, 255, 239, 0, 209, 255, 255, 143, 0, 251, 255, 255, 111, 16, 255, 255, 255, 255, 255, 255, 255, 255, 159, 65, 255, 255, 255, 47, 0, 252, 255, 255, 143, 64, 255, 255 };
const uint8_t * const Font_Roboto_Bold_20[126 + 1 - 32] = {
Font_Roboto_Bold_20_glyph_32,

@ -102,7 +102,7 @@
/* } */ static const uint8_t Font_Roboto_Regular_20_glyph_125[] = { 7, 20, 7, 0, 16, 152, 1, 0, 0, 228, 29, 0, 0, 48, 159, 0, 0, 0, 238, 0, 0, 0, 252, 0, 0, 0, 252, 0, 0, 0, 252, 0, 0, 0, 250, 2, 0, 0, 244, 27, 0, 0, 112, 255, 1, 0, 209, 143, 0, 0, 248, 5, 0, 0, 251, 1, 0, 0, 252, 0, 0, 0, 252, 0, 0, 0, 252, 0, 0, 0, 222, 0, 0, 80, 143, 0, 0, 229, 29, 0, 0, 152, 1, 0, 0 };
/* ~ */ static const uint8_t Font_Roboto_Regular_20_glyph_126[] = { 12, 4, 14, 1, 8, 64, 253, 109, 0, 32, 59, 243, 173, 254, 43, 161, 31, 249, 1, 144, 255, 255, 7, 50, 0, 0, 115, 56, 0 };
const uint8_t Font_Roboto_Regular_20_glyph_nonprintable[] = { 9, 14, 9, 0, 14, 255, 57, 16, 213, 255, 111, 16, 69, 16, 254, 14, 242, 255, 8, 247, 125, 250, 255, 12, 245, 255, 255, 255, 10, 248, 255, 255, 239, 1, 254, 255, 255, 46, 160, 255, 255, 255, 3, 250, 255, 255, 191, 96, 255, 255, 255, 143, 160, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 159, 160, 255, 255, 255, 159, 160, 255, 255 };
/* ? */ const uint8_t Font_Roboto_Regular_20_glyph_nonprintable[] = { 9, 14, 9, 0, 14, 255, 57, 16, 213, 255, 111, 16, 69, 16, 254, 14, 242, 255, 8, 247, 125, 250, 255, 12, 245, 255, 255, 255, 10, 248, 255, 255, 239, 1, 254, 255, 255, 46, 160, 255, 255, 255, 3, 250, 255, 255, 191, 96, 255, 255, 255, 143, 160, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 159, 160, 255, 255, 255, 159, 160, 255, 255 };
const uint8_t * const Font_Roboto_Regular_20[126 + 1 - 32] = {
Font_Roboto_Regular_20_glyph_32,

@ -102,7 +102,7 @@
/* } */ static const uint8_t Font_RobotoMono_Medium_20_glyph_125[] = { 7, 20, 12, 3, 16, 117, 1, 0, 0, 249, 30, 0, 0, 112, 175, 0, 0, 16, 255, 0, 0, 0, 255, 2, 0, 0, 255, 3, 0, 0, 255, 3, 0, 0, 254, 4, 0, 0, 248, 28, 0, 0, 144, 255, 11, 0, 144, 255, 10, 0, 248, 28, 0, 0, 254, 4, 0, 0, 255, 3, 0, 0, 255, 3, 0, 0, 255, 2, 0, 32, 255, 0, 0, 144, 159, 0, 0, 250, 29, 0, 0, 100, 0, 0, 0 };
/* ~ */ static const uint8_t Font_RobotoMono_Medium_20_glyph_126[] = { 12, 5, 12, 0, 8, 64, 237, 76, 0, 32, 22, 242, 255, 255, 8, 144, 63, 248, 6, 229, 239, 253, 14, 149, 0, 32, 251, 239, 3, 0, 0, 0, 16, 2, 0 };
const uint8_t Font_RobotoMono_Medium_20_glyph_nonprintable[] = { 10, 16, 12, 1, 15, 255, 207, 153, 251, 255, 239, 3, 0, 16, 250, 47, 0, 102, 2, 208, 12, 176, 255, 14, 128, 174, 250, 255, 47, 112, 255, 255, 255, 13, 160, 255, 255, 255, 3, 242, 255, 255, 79, 16, 253, 255, 255, 5, 209, 255, 255, 255, 0, 251, 255, 255, 255, 34, 254, 255, 255, 255, 255, 255, 255, 255, 255, 153, 255, 255, 255, 175, 0, 250, 255, 255, 207, 0, 253, 255, 255, 255, 255, 255, 255 };
/* ? */ const uint8_t Font_RobotoMono_Medium_20_glyph_nonprintable[] = { 10, 16, 12, 1, 15, 255, 207, 153, 251, 255, 239, 3, 0, 16, 250, 47, 0, 102, 2, 208, 12, 176, 255, 14, 128, 174, 250, 255, 47, 112, 255, 255, 255, 13, 160, 255, 255, 255, 3, 242, 255, 255, 79, 16, 253, 255, 255, 5, 209, 255, 255, 255, 0, 251, 255, 255, 255, 34, 254, 255, 255, 255, 255, 255, 255, 255, 255, 153, 255, 255, 255, 175, 0, 250, 255, 255, 207, 0, 253, 255, 255, 255, 255, 255, 255 };
const uint8_t * const Font_RobotoMono_Medium_20[126 + 1 - 32] = {
Font_RobotoMono_Medium_20_glyph_32,

@ -102,7 +102,7 @@
/* } */ static const uint8_t Font_TTHoves_Bold_17_glyph_125[] = { 7, 17, 7, 0, 13, 65, 3, 0, 0, 246, 223, 1, 0, 246, 255, 10, 0, 16, 254, 13, 0, 0, 251, 13, 0, 0, 251, 13, 0, 0, 251, 14, 0, 0, 248, 207, 0, 0, 128, 255, 0, 0, 210, 255, 0, 0, 249, 127, 0, 0, 251, 14, 0, 0, 251, 13, 0, 0, 251, 13, 0, 81, 255, 12, 0, 246, 255, 8, 0, 246, 158, 0, 0 };
/* ~ */ static const uint8_t Font_TTHoves_Bold_17_glyph_126[] = { 10, 4, 10, 0, 7, 0, 98, 2, 0, 0, 128, 255, 159, 246, 143, 242, 255, 255, 255, 79, 245, 63, 180, 239, 7 };
const uint8_t Font_TTHoves_Bold_17_glyph_nonprintable[] = { 9, 12, 9, 0, 12, 255, 40, 32, 230, 255, 63, 0, 0, 16, 254, 10, 32, 39, 0, 248, 90, 213, 207, 0, 246, 255, 255, 95, 0, 249, 255, 223, 2, 96, 255, 255, 95, 0, 250, 255, 255, 47, 64, 255, 255, 255, 239, 238, 255, 255, 255, 63, 99, 255, 255, 255, 15, 48, 255, 255, 255, 15, 48, 255, 255 };
/* ? */ const uint8_t Font_TTHoves_Bold_17_glyph_nonprintable[] = { 9, 12, 9, 0, 12, 255, 40, 32, 230, 255, 63, 0, 0, 16, 254, 10, 32, 39, 0, 248, 90, 213, 207, 0, 246, 255, 255, 95, 0, 249, 255, 223, 2, 96, 255, 255, 95, 0, 250, 255, 255, 47, 64, 255, 255, 255, 239, 238, 255, 255, 255, 63, 99, 255, 255, 255, 15, 48, 255, 255, 255, 15, 48, 255, 255 };
const uint8_t * const Font_TTHoves_Bold_17[126 + 1 - 32] = {
Font_TTHoves_Bold_17_glyph_32,

@ -102,7 +102,7 @@
/* } */ static const uint8_t Font_TTHoves_DemiBold_21_glyph_125[] = { 7, 20, 8, 0, 16, 97, 37, 0, 0, 244, 255, 8, 0, 244, 255, 47, 0, 0, 248, 79, 0, 0, 244, 95, 0, 0, 244, 95, 0, 0, 244, 95, 0, 0, 244, 95, 0, 0, 242, 175, 1, 0, 160, 255, 12, 0, 0, 249, 12, 0, 208, 255, 10, 0, 243, 143, 0, 0, 244, 95, 0, 0, 244, 95, 0, 0, 244, 95, 0, 0, 245, 95, 0, 97, 252, 79, 0, 244, 255, 14, 0, 244, 207, 3, 0 };
/* ~ */ static const uint8_t Font_TTHoves_DemiBold_21_glyph_126[] = { 12, 5, 12, 0, 8, 0, 215, 158, 1, 144, 73, 112, 255, 255, 61, 245, 111, 224, 255, 254, 255, 255, 47, 241, 143, 64, 254, 255, 9, 64, 37, 0, 113, 72, 0 };
const uint8_t Font_TTHoves_DemiBold_21_glyph_nonprintable[] = { 11, 15, 11, 0, 15, 255, 93, 2, 98, 254, 255, 175, 0, 0, 0, 192, 255, 30, 0, 100, 2, 16, 255, 11, 80, 255, 63, 0, 252, 157, 217, 255, 127, 0, 250, 255, 255, 255, 30, 0, 253, 255, 255, 159, 0, 80, 255, 255, 255, 9, 0, 248, 255, 255, 255, 2, 176, 255, 255, 255, 255, 0, 244, 255, 255, 255, 255, 85, 249, 255, 255, 255, 255, 255, 255, 255, 255, 255, 239, 17, 245, 255, 255, 255, 239, 0, 244, 255, 255, 255, 239, 0, 244, 255, 255 };
/* ? */ const uint8_t Font_TTHoves_DemiBold_21_glyph_nonprintable[] = { 11, 15, 11, 0, 15, 255, 93, 2, 98, 254, 255, 175, 0, 0, 0, 192, 255, 30, 0, 100, 2, 16, 255, 11, 80, 255, 63, 0, 252, 157, 217, 255, 127, 0, 250, 255, 255, 255, 30, 0, 253, 255, 255, 159, 0, 80, 255, 255, 255, 9, 0, 248, 255, 255, 255, 2, 176, 255, 255, 255, 255, 0, 244, 255, 255, 255, 255, 85, 249, 255, 255, 255, 255, 255, 255, 255, 255, 255, 239, 17, 245, 255, 255, 255, 239, 0, 244, 255, 255, 255, 239, 0, 244, 255, 255 };
const uint8_t * const Font_TTHoves_DemiBold_21[126 + 1 - 32] = {
Font_TTHoves_DemiBold_21_glyph_32,

@ -102,7 +102,7 @@
/* } */ static const uint8_t Font_TTHoves_Regular_21_glyph_125[] = { 6, 20, 7, 0, 16, 81, 3, 0, 244, 143, 0, 0, 250, 0, 0, 247, 1, 0, 247, 1, 0, 247, 1, 0, 247, 1, 0, 247, 1, 0, 246, 3, 0, 209, 77, 0, 48, 126, 0, 243, 25, 0, 247, 1, 0, 247, 1, 0, 247, 1, 0, 247, 1, 0, 247, 1, 0, 247, 1, 97, 237, 0, 244, 77, 0 };
/* ~ */ static const uint8_t Font_TTHoves_Regular_21_glyph_126[] = { 11, 4, 12, 0, 8, 0, 97, 21, 0, 48, 5, 64, 255, 239, 3, 208, 12, 192, 61, 211, 159, 251, 7, 224, 8, 0, 233, 142, 0 };
const uint8_t Font_TTHoves_Regular_21_glyph_nonprintable[] = { 10, 15, 11, 0, 15, 255, 76, 17, 163, 255, 159, 0, 84, 1, 246, 14, 226, 255, 79, 176, 10, 250, 255, 239, 96, 239, 255, 255, 255, 64, 255, 255, 255, 207, 96, 255, 255, 255, 46, 192, 255, 255, 175, 1, 251, 255, 255, 10, 211, 255, 255, 255, 4, 254, 255, 255, 255, 19, 255, 255, 255, 255, 119, 255, 255, 255, 255, 255, 255, 255, 255, 255, 35, 255, 255, 255, 255, 1, 255, 255 };
/* ? */ const uint8_t Font_TTHoves_Regular_21_glyph_nonprintable[] = { 10, 15, 11, 0, 15, 255, 76, 17, 163, 255, 159, 0, 84, 1, 246, 14, 226, 255, 79, 176, 10, 250, 255, 239, 96, 239, 255, 255, 255, 64, 255, 255, 255, 207, 96, 255, 255, 255, 46, 192, 255, 255, 175, 1, 251, 255, 255, 10, 211, 255, 255, 255, 4, 254, 255, 255, 255, 19, 255, 255, 255, 255, 119, 255, 255, 255, 255, 255, 255, 255, 255, 255, 35, 255, 255, 255, 255, 1, 255, 255 };
const uint8_t * const Font_TTHoves_Regular_21[126 + 1 - 32] = {
Font_TTHoves_Regular_21_glyph_32,

@ -102,7 +102,7 @@
/* } */ 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 Font_Unifont_Bold_16_glyph_nonprintable[] = { 6, 10, 7, 0, 10, 5, 199, 60, 231, 60, 255, 207, 63 };
const uint8_t * const Font_Unifont_Bold_16[126 + 1 - 32] = {
Font_Unifont_Bold_16_glyph_32,

@ -106,7 +106,7 @@
/* } */ 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 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,

@ -18,39 +18,15 @@
*/
#include "fonts.h"
#include <assert.h>
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
#ifdef TRANSLATIONS
#include "librust_fonts.h"
#endif
static uint8_t convert_char(const uint8_t c) {
static char last_was_utf8 = 0;
// non-printable ASCII character
if (c < ' ') {
last_was_utf8 = 0;
return 0x7F;
}
// regular ASCII character
if (c < 0x80) {
last_was_utf8 = 0;
return c;
}
// UTF-8 handling: https://en.wikipedia.org/wiki/UTF-8#Encoding
// bytes 11xxxxxx are first bytes of UTF-8 characters
if (c >= 0xC0) {
last_was_utf8 = 1;
return 0x7F;
}
if (last_was_utf8) {
// bytes 10xxxxxx can be successive UTF-8 characters ...
return 0; // skip glyph
} else {
// ... or they are just non-printable ASCII characters
return 0x7F;
}
}
#define UNICODE_BADCHAR 0xFFFD
int font_height(int font) {
switch (font) {
@ -130,9 +106,117 @@ int font_baseline(int font) {
return 0;
}
const uint8_t *font_get_glyph(int font, uint8_t c) {
c = convert_char(c);
if (!c) return 0;
font_glyph_iter_t font_glyph_iter_init(const int font, const uint8_t *text,
const int len) {
return (font_glyph_iter_t){
.font = font,
.text = text,
.remaining = len,
};
}
#define IS_UTF8_CONTINUE(c) (((c)&0b11000000) == 0b10000000)
static uint16_t next_utf8_codepoint(font_glyph_iter_t *iter) {
uint16_t out;
assert(iter->remaining > 0);
// 1-byte UTF-8 character
if (iter->text[0] < 0x7f) {
out = iter->text[0];
++iter->text;
--iter->remaining;
return out;
}
// 2-byte UTF-8 character
if (iter->remaining >= 2 && ((iter->text[0] & 0b11100000) == 0b11000000) &&
IS_UTF8_CONTINUE(iter->text[1])) {
out = (((uint16_t)iter->text[0] & 0b00011111) << 6) |
(iter->text[1] & 0b00111111);
iter->text += 2;
iter->remaining -= 2;
return out;
}
// 3-byte UTF-8 character
if (iter->remaining >= 3 && ((iter->text[0] & 0b11110000) == 0b11100000) &&
IS_UTF8_CONTINUE(iter->text[1]) && IS_UTF8_CONTINUE(iter->text[2])) {
out = (((uint16_t)iter->text[0] & 0b00001111) << 12) |
(((uint16_t)iter->text[1] & 0b00111111) << 6) |
(iter->text[2] & 0b00111111);
iter->text += 3;
iter->remaining -= 3;
return out;
}
// 4-byte UTF-8 character
if (iter->remaining >= 4 && ((iter->text[0] & 0b11111000) == 0b11110000) &&
IS_UTF8_CONTINUE(iter->text[1]) && IS_UTF8_CONTINUE(iter->text[2]) &&
IS_UTF8_CONTINUE(iter->text[3])) {
// we use 16-bit codepoints, so we can't represent 4-byte UTF-8 characters
iter->text += 4;
iter->remaining -= 4;
return UNICODE_BADCHAR;
}
++iter->text;
--iter->remaining;
return UNICODE_BADCHAR;
}
bool font_next_glyph(font_glyph_iter_t *iter, const uint8_t **out) {
if (iter->remaining <= 0) {
return false;
}
uint16_t c = next_utf8_codepoint(iter);
*out = font_get_glyph(iter->font, c);
if (*out == NULL) {
// should not happen but ¯\_(ツ)_/¯
return font_next_glyph(iter, out);
} else {
return true;
}
}
const uint8_t *font_nonprintable_glyph(int font) {
#define PASTER(s) s##_glyph_nonprintable
#define NONPRINTABLE_GLYPH(s) PASTER(s)
switch (font) {
#ifdef TREZOR_FONT_NORMAL_ENABLE
case FONT_NORMAL:
return NONPRINTABLE_GLYPH(FONT_NORMAL_DATA);
#endif
#ifdef TREZOR_FONT_DEMIBOLD_ENABLE
case FONT_DEMIBOLD:
return NONPRINTABLE_GLYPH(FONT_DEMIBOLD_DATA);
#endif
#ifdef TREZOR_FONT_BOLD_ENABLE
case FONT_BOLD:
return NONPRINTABLE_GLYPH(FONT_BOLD_DATA);
#endif
#ifdef TREZOR_FONT_MONO_ENABLE
case FONT_MONO:
return NONPRINTABLE_GLYPH(FONT_MONO_DATA);
#endif
#ifdef TREZOR_FONT_BIG_ENABLE
case FONT_BIG:
return NONPRINTABLE_GLYPH(FONT_BIG_DATA);
#endif
default:
return NULL;
}
}
const uint8_t *font_get_glyph(int font, uint16_t c) {
#ifdef TRANSLATIONS
// found UTF8 character
// it is not hardcoded in firmware fonts, it must be extracted from the
// embedded blob
if (c >= 0x7F) {
const uint8_t *g = get_utf8_glyph(c, font);
if (g != NULL) {
return g;
}
}
#endif
// printable ASCII character
if (c >= ' ' && c < 0x7F) {
@ -161,29 +245,7 @@ const uint8_t *font_get_glyph(int font, uint8_t c) {
return 0;
}
// non-printable character
#define PASTER(s) s##_glyph_nonprintable
#define NONPRINTABLE_GLYPH(s) PASTER(s)
switch (font) {
#ifdef TREZOR_FONT_NORMAL_ENABLE
case FONT_NORMAL:
return NONPRINTABLE_GLYPH(FONT_NORMAL_DATA);
#endif
#ifdef TREZOR_FONT_DEMIBOLD_ENABLE
case FONT_DEMIBOLD:
return NONPRINTABLE_GLYPH(FONT_DEMIBOLD_DATA);
#endif
#ifdef TREZOR_FONT_BOLD_ENABLE
case FONT_BOLD:
return NONPRINTABLE_GLYPH(FONT_BOLD_DATA);
#endif
#ifdef TREZOR_FONT_MONO_ENABLE
case FONT_MONO:
return NONPRINTABLE_GLYPH(FONT_MONO_DATA);
#endif
}
return 0;
return font_nonprintable_glyph(font);
}
// compute the width of the text (in pixels)
@ -193,50 +255,11 @@ int font_text_width(int font, const char *text, int textlen) {
if (textlen < 0) {
textlen = strlen(text);
}
for (int i = 0; i < textlen; i++) {
const uint8_t *g = font_get_glyph(font, (uint8_t)text[i]);
if (!g) continue;
font_glyph_iter_t iter = font_glyph_iter_init(font, (uint8_t *)text, textlen);
const uint8_t *g = NULL;
while (font_next_glyph(&iter, &g)) {
const uint8_t adv = g[2]; // advance
width += adv;
/*
if (i != textlen - 1) {
const uint8_t adv = g[2]; // advance
width += adv;
} else { // last character
const uint8_t w = g[0]; // width
const uint8_t bearX = g[3]; // bearingX
width += (bearX + w);
}
*/
}
return width;
}
// Returns how many characters of the string can be used before exceeding
// the requested width. Tries to avoid breaking words if possible.
int font_text_split(int font, const char *text, int textlen,
int requested_width) {
int width = 0;
int lastspace = 0;
// determine text length if not provided
if (textlen < 0) {
textlen = strlen(text);
}
for (int i = 0; i < textlen; i++) {
if (text[i] == ' ') {
lastspace = i;
}
const uint8_t *g = font_get_glyph(font, (uint8_t)text[i]);
if (!g) continue;
const uint8_t adv = g[2]; // advance
width += adv;
if (width > requested_width) {
if (lastspace > 0) {
return lastspace;
} else {
return i;
}
}
}
return textlen;
}

@ -20,6 +20,8 @@
#ifndef _FONTS_H
#define _FONTS_H
#include <stdbool.h>
#include "fonts/font_bitmap.h"
#include TREZOR_BOARD
@ -117,9 +119,18 @@
int font_height(int font);
int font_max_height(int font);
int font_baseline(int font);
const uint8_t *font_get_glyph(int font, uint8_t c);
const uint8_t *font_get_glyph(int font, uint16_t c);
const uint8_t *font_nonprintable_glyph(int font);
typedef struct {
const int font;
const uint8_t *text;
int remaining;
} font_glyph_iter_t;
font_glyph_iter_t font_glyph_iter_init(const int font, const uint8_t *text,
const int len);
bool font_next_glyph(font_glyph_iter_t *iter, const uint8_t **out);
int font_text_width(int font, const char *text, int textlen);
int font_text_split(int font, const char *text, int textlen,
int requested_width);
#endif //_FONTS_H

@ -334,6 +334,7 @@ secbool check_firmware_header(const uint8_t *header, size_t header_size,
info->ver_major = ihdr->version & 0xFF;
info->ver_minor = (ihdr->version >> 8) & 0xFF;
info->ver_patch = (ihdr->version >> 16) & 0xFF;
info->ver_build = (ihdr->version >> 24) & 0xFF;
// calculate and copy the image fingerprint
get_image_fingerprint(ihdr, info->fingerprint);

@ -88,6 +88,7 @@ typedef struct {
uint8_t ver_major;
uint8_t ver_minor;
uint8_t ver_patch;
uint8_t ver_build;
// firmware fingerprint
uint8_t fingerprint[BLAKE2S_DIGEST_LENGTH];
// hash of vendor and image header

@ -15,6 +15,7 @@
extern const flash_area_t STORAGE_AREAS[STORAGE_AREAS_COUNT];
extern const flash_area_t BOARDLOADER_AREA;
extern const flash_area_t SECRET_AREA;
extern const flash_area_t TRANSLATIONS_AREA;
extern const flash_area_t BOOTLOADER_AREA;
extern const flash_area_t FIRMWARE_AREA;
extern const flash_area_t WIPE_AREA;

@ -38,6 +38,15 @@ const flash_area_t SECRET_AREA = {
},
};
const flash_area_t TRANSLATIONS_AREA = {
.num_subareas = 1,
.subarea[0] =
{
.first_sector = 13,
.num_sectors = 2,
},
};
const flash_area_t BOOTLOADER_AREA = {
.num_subareas = 1,
.subarea[0] =
@ -62,7 +71,7 @@ const flash_area_t FIRMWARE_AREA = {
};
const flash_area_t WIPE_AREA = {
.num_subareas = 4,
.num_subareas = 3,
.subarea[0] =
{
.first_sector = 4,
@ -74,12 +83,6 @@ const flash_area_t WIPE_AREA = {
.num_sectors = 6,
},
.subarea[2] =
{
.first_sector = 13,
.num_sectors = 2, // sector 15 skipped due to bootloader MPU
// settings, sector 12 is secret
},
.subarea[3] =
{
.first_sector = 16,
.num_sectors = 8,

@ -29,6 +29,15 @@ const flash_area_t BOARDLOADER_AREA = {
},
};
const flash_area_t TRANSLATIONS_AREA = {
.num_subareas = 1,
.subarea[0] =
{
.first_sector = 12,
.num_sectors = 3,
},
};
const flash_area_t BOOTLOADER_AREA = {
.num_subareas = 1,
.subarea[0] =

@ -32,6 +32,7 @@ rgb_led = []
backlight = []
usb = []
optiga = []
translations = []
test = [
"button",
"cc",
@ -43,7 +44,8 @@ test = [
"dma2d",
"touch",
"backlight",
"optiga"
"optiga",
"translations",
]
[lib]

@ -289,6 +289,10 @@ fn generate_trezorhal_bindings() {
.allowlist_function("storage_delete")
.allowlist_function("storage_set_counter")
.allowlist_function("storage_next_counter")
.allowlist_function("translations_read")
.allowlist_function("translations_write")
.allowlist_function("translations_erase")
.allowlist_function("translations_area_bytesize")
// display
.allowlist_function("display_clear")
.allowlist_function("display_offset")

@ -9,6 +9,7 @@ mp_obj_t protobuf_debug_msg_def_type();
extern mp_obj_module_t mp_module_trezorproto;
extern mp_obj_module_t mp_module_trezorui2;
extern mp_obj_module_t mp_module_trezortranslate;
#ifdef TREZOR_EMULATOR
mp_obj_t ui_debug_layout_type();

@ -0,0 +1 @@
const uint8_t *get_utf8_glyph(uint16_t char_code, int font);

@ -5,6 +5,7 @@
#pragma GCC diagnostic ignored "-Wunused-function"
static void _librust_qstrs(void) {
MP_QSTR_;
MP_QSTR_CANCELLED;
MP_QSTR_CONFIRMED;
MP_QSTR_INFO;
@ -13,6 +14,8 @@ static void _librust_qstrs(void) {
MP_QSTR_MESSAGE_WIRE_TYPE;
MP_QSTR_Msg;
MP_QSTR_MsgDef;
MP_QSTR_TR;
MP_QSTR_TranslationsHeader;
MP_QSTR___dict__;
MP_QSTR___name__;
MP_QSTR_account;
@ -20,7 +23,20 @@ static void _librust_qstrs(void) {
MP_QSTR_accounts;
MP_QSTR_action;
MP_QSTR_active;
MP_QSTR_addr_mismatch__contact_support_at;
MP_QSTR_addr_mismatch__key_mismatch;
MP_QSTR_addr_mismatch__mismatch;
MP_QSTR_addr_mismatch__support_url;
MP_QSTR_addr_mismatch__wrong_derivation_path;
MP_QSTR_addr_mismatch__xpub_mismatch;
MP_QSTR_address;
MP_QSTR_address__public_key;
MP_QSTR_address__title_cosigner;
MP_QSTR_address__title_receive_address;
MP_QSTR_address__title_yours;
MP_QSTR_address_details__derivation_path;
MP_QSTR_address_details__title_receive_address;
MP_QSTR_address_details__title_receiving_to;
MP_QSTR_address_label;
MP_QSTR_address_title;
MP_QSTR_allow_cancel;
@ -31,16 +47,224 @@ static void _librust_qstrs(void) {
MP_QSTR_amount_title;
MP_QSTR_amount_value;
MP_QSTR_app_name;
MP_QSTR_area_bytesize;
MP_QSTR_attach_timer_fn;
MP_QSTR_authenticate__confirm_template;
MP_QSTR_authenticate__header;
MP_QSTR_auto_lock__change_template;
MP_QSTR_auto_lock__title;
MP_QSTR_backup__can_back_up_anytime;
MP_QSTR_backup__it_should_be_backed_up;
MP_QSTR_backup__it_should_be_backed_up_now;
MP_QSTR_backup__new_wallet_created;
MP_QSTR_backup__new_wallet_successfully_created;
MP_QSTR_backup__recover_anytime;
MP_QSTR_backup__title_backup_wallet;
MP_QSTR_backup__title_skip;
MP_QSTR_backup__want_to_skip;
MP_QSTR_binance__buy;
MP_QSTR_binance__confirm_cancel;
MP_QSTR_binance__confirm_input;
MP_QSTR_binance__confirm_order;
MP_QSTR_binance__confirm_output;
MP_QSTR_binance__order_id;
MP_QSTR_binance__pair;
MP_QSTR_binance__price;
MP_QSTR_binance__quantity;
MP_QSTR_binance__sell;
MP_QSTR_binance__sender_address;
MP_QSTR_binance__side;
MP_QSTR_bitcoin__commitment_data;
MP_QSTR_bitcoin__confirm_locktime;
MP_QSTR_bitcoin__create_proof_of_ownership;
MP_QSTR_bitcoin__high_mining_fee_template;
MP_QSTR_bitcoin__locktime_no_effect;
MP_QSTR_bitcoin__locktime_set_to;
MP_QSTR_bitcoin__locktime_set_to_blockheight;
MP_QSTR_bitcoin__lot_of_change_outputs;
MP_QSTR_bitcoin__multiple_accounts;
MP_QSTR_bitcoin__new_fee_rate;
MP_QSTR_bitcoin__simple_send_of;
MP_QSTR_bitcoin__ticket_amount;
MP_QSTR_bitcoin__title_confirm_details;
MP_QSTR_bitcoin__title_finalize_transaction;
MP_QSTR_bitcoin__title_high_mining_fee;
MP_QSTR_bitcoin__title_meld_transaction;
MP_QSTR_bitcoin__title_modify_amount;
MP_QSTR_bitcoin__title_payjoin;
MP_QSTR_bitcoin__title_proof_of_ownership;
MP_QSTR_bitcoin__title_purchase_ticket;
MP_QSTR_bitcoin__title_update_transaction;
MP_QSTR_bitcoin__unknown_path;
MP_QSTR_bitcoin__unknown_transaction;
MP_QSTR_bitcoin__unusually_high_fee;
MP_QSTR_bitcoin__unverified_external_inputs;
MP_QSTR_bitcoin__valid_signature;
MP_QSTR_bitcoin__voting_rights;
MP_QSTR_bootscreen;
MP_QSTR_bounds;
MP_QSTR_button;
MP_QSTR_button_event;
MP_QSTR_buttons__abort;
MP_QSTR_buttons__access;
MP_QSTR_buttons__again;
MP_QSTR_buttons__allow;
MP_QSTR_buttons__back;
MP_QSTR_buttons__back_up;
MP_QSTR_buttons__cancel;
MP_QSTR_buttons__change;
MP_QSTR_buttons__check;
MP_QSTR_buttons__check_again;
MP_QSTR_buttons__close;
MP_QSTR_buttons__confirm;
MP_QSTR_buttons__continue;
MP_QSTR_buttons__details;
MP_QSTR_buttons__enable;
MP_QSTR_buttons__enter;
MP_QSTR_buttons__enter_share;
MP_QSTR_buttons__export;
MP_QSTR_buttons__format;
MP_QSTR_buttons__go_back;
MP_QSTR_buttons__hold_to_confirm;
MP_QSTR_buttons__info;
MP_QSTR_buttons__install;
MP_QSTR_buttons__more_info;
MP_QSTR_buttons__ok_i_understand;
MP_QSTR_buttons__purchase;
MP_QSTR_buttons__quit;
MP_QSTR_buttons__restart;
MP_QSTR_buttons__retry;
MP_QSTR_buttons__select;
MP_QSTR_buttons__set;
MP_QSTR_buttons__show_all;
MP_QSTR_buttons__show_details;
MP_QSTR_buttons__show_words;
MP_QSTR_buttons__skip;
MP_QSTR_buttons__try_again;
MP_QSTR_buttons__turn_off;
MP_QSTR_buttons__turn_on;
MP_QSTR_cancel_arrow;
MP_QSTR_cancel_cross;
MP_QSTR_cardano__addr_base;
MP_QSTR_cardano__addr_enterprise;
MP_QSTR_cardano__addr_legacy;
MP_QSTR_cardano__addr_pointer;
MP_QSTR_cardano__addr_reward;
MP_QSTR_cardano__address_no_staking;
MP_QSTR_cardano__amount_burned_decimals_unknown;
MP_QSTR_cardano__amount_minted_decimals_unknown;
MP_QSTR_cardano__amount_sent_decimals_unknown;
MP_QSTR_cardano__anonymous_pool;
MP_QSTR_cardano__asset_fingerprint;
MP_QSTR_cardano__auxiliary_data_hash;
MP_QSTR_cardano__block;
MP_QSTR_cardano__catalyst;
MP_QSTR_cardano__certificate;
MP_QSTR_cardano__change_output;
MP_QSTR_cardano__check_all_items;
MP_QSTR_cardano__choose_level_of_details;
MP_QSTR_cardano__collateral_input_id;
MP_QSTR_cardano__collateral_input_index;
MP_QSTR_cardano__collateral_output_contains_tokens;
MP_QSTR_cardano__collateral_return;
MP_QSTR_cardano__confirm;
MP_QSTR_cardano__confirm_signing_stake_pool;
MP_QSTR_cardano__confirm_transaction;
MP_QSTR_cardano__confirming_a_multisig_transaction;
MP_QSTR_cardano__confirming_a_plutus_transaction;
MP_QSTR_cardano__confirming_pool_registration;
MP_QSTR_cardano__confirming_transction;
MP_QSTR_cardano__cost;
MP_QSTR_cardano__credential_mismatch;
MP_QSTR_cardano__datum_hash;
MP_QSTR_cardano__delegating_to;
MP_QSTR_cardano__for_account_and_index_template;
MP_QSTR_cardano__for_account_template;
MP_QSTR_cardano__for_key_hash;
MP_QSTR_cardano__for_script;
MP_QSTR_cardano__inline_datum;
MP_QSTR_cardano__input_id;
MP_QSTR_cardano__input_index;
MP_QSTR_cardano__intro_text_change;
MP_QSTR_cardano__intro_text_owned_by_device;
MP_QSTR_cardano__intro_text_registration_payment;
MP_QSTR_cardano__key_hash;
MP_QSTR_cardano__margin;
MP_QSTR_cardano__multisig_path;
MP_QSTR_cardano__nested_scripts_template;
MP_QSTR_cardano__network;
MP_QSTR_cardano__no_output_tx;
MP_QSTR_cardano__nonce;
MP_QSTR_cardano__other;
MP_QSTR_cardano__path;
MP_QSTR_cardano__pledge;
MP_QSTR_cardano__pointer;
MP_QSTR_cardano__policy_id;
MP_QSTR_cardano__pool_metadata_hash;
MP_QSTR_cardano__pool_metadata_url;
MP_QSTR_cardano__pool_owner;
MP_QSTR_cardano__pool_reward_account;
MP_QSTR_cardano__reference_input_id;
MP_QSTR_cardano__reference_input_index;
MP_QSTR_cardano__reference_script;
MP_QSTR_cardano__required_signer;
MP_QSTR_cardano__reward;
MP_QSTR_cardano__reward_address;
MP_QSTR_cardano__reward_eligibility_warning;
MP_QSTR_cardano__rewards_go_to;
MP_QSTR_cardano__script;
MP_QSTR_cardano__script_all;
MP_QSTR_cardano__script_any;
MP_QSTR_cardano__script_data_hash;
MP_QSTR_cardano__script_hash;
MP_QSTR_cardano__script_invalid_before;
MP_QSTR_cardano__script_invalid_hereafter;
MP_QSTR_cardano__script_key;
MP_QSTR_cardano__script_n_of_k;
MP_QSTR_cardano__script_reward;
MP_QSTR_cardano__sending;
MP_QSTR_cardano__show_simple;
MP_QSTR_cardano__sign_tx_path_template;
MP_QSTR_cardano__stake_delegation;
MP_QSTR_cardano__stake_deregistration;
MP_QSTR_cardano__stake_pool_registration;
MP_QSTR_cardano__stake_pool_registration_pool_id;
MP_QSTR_cardano__stake_registration;
MP_QSTR_cardano__staking_key_for_account;
MP_QSTR_cardano__to_pool;
MP_QSTR_cardano__token_minting_path;
MP_QSTR_cardano__total_collateral;
MP_QSTR_cardano__transaction;
MP_QSTR_cardano__transaction_contains_minting_or_burning;
MP_QSTR_cardano__transaction_contains_script_address_no_datum;
MP_QSTR_cardano__transaction_fee;
MP_QSTR_cardano__transaction_id;
MP_QSTR_cardano__transaction_no_collateral_input;
MP_QSTR_cardano__transaction_no_script_data_hash;
MP_QSTR_cardano__transaction_output_contains_tokens;
MP_QSTR_cardano__ttl;
MP_QSTR_cardano__unknown_collateral_amount;
MP_QSTR_cardano__unusual_path;
MP_QSTR_cardano__valid_since;
MP_QSTR_cardano__verify_script;
MP_QSTR_cardano__vote_key_registration;
MP_QSTR_cardano__vote_public_key;
MP_QSTR_cardano__voting_purpose;
MP_QSTR_cardano__warning;
MP_QSTR_cardano__weight;
MP_QSTR_cardano__withdrawal_for_address_template;
MP_QSTR_cardano__x_of_y_signatures_template;
MP_QSTR_case_sensitive;
MP_QSTR_check_homescreen_format;
MP_QSTR_chunkify;
MP_QSTR_coinjoin__access_account;
MP_QSTR_coinjoin__do_not_disconnect;
MP_QSTR_coinjoin__max_mining_fee;
MP_QSTR_coinjoin__max_rounds;
MP_QSTR_coinjoin__title;
MP_QSTR_coinjoin__title_do_not_disconnect;
MP_QSTR_coinjoin__title_progress;
MP_QSTR_coinjoin__waiting_for_others;
MP_QSTR_coinjoin_authorized;
MP_QSTR_confirm_action;
MP_QSTR_confirm_address;
@ -61,60 +285,507 @@ static void _librust_qstrs(void) {
MP_QSTR_confirm_recovery;
MP_QSTR_confirm_reset_device;
MP_QSTR_confirm_total;
MP_QSTR_confirm_total__fee_rate;
MP_QSTR_confirm_total__sending_from_account;
MP_QSTR_confirm_total__title_fee;
MP_QSTR_confirm_total__title_sending_from;
MP_QSTR_confirm_value;
MP_QSTR_confirm_with_info;
MP_QSTR_count;
MP_QSTR_data;
MP_QSTR_data_hash;
MP_QSTR_data_len;
MP_QSTR_debug__loading_seed;
MP_QSTR_debug__loading_seed_not_recommended;
MP_QSTR_decode;
MP_QSTR_deinit;
MP_QSTR_description;
MP_QSTR_details_title;
MP_QSTR_device_name__change_template;
MP_QSTR_device_name__title;
MP_QSTR_disable_animation;
MP_QSTR_dry_run;
MP_QSTR_encode;
MP_QSTR_encoded_length;
MP_QSTR_entropy__send;
MP_QSTR_entropy__title;
MP_QSTR_entropy__title_confirm;
MP_QSTR_eos__about_to_sign_template;
MP_QSTR_eos__action_name;
MP_QSTR_eos__arbitrary_data;
MP_QSTR_eos__buy_ram;
MP_QSTR_eos__bytes;
MP_QSTR_eos__cancel_vote;
MP_QSTR_eos__checksum;
MP_QSTR_eos__code;
MP_QSTR_eos__contract;
MP_QSTR_eos__cpu;
MP_QSTR_eos__creator;
MP_QSTR_eos__delegate;
MP_QSTR_eos__delete_auth;
MP_QSTR_eos__from;
MP_QSTR_eos__link_auth;
MP_QSTR_eos__memo;
MP_QSTR_eos__name;
MP_QSTR_eos__net;
MP_QSTR_eos__new_account;
MP_QSTR_eos__owner;
MP_QSTR_eos__parent;
MP_QSTR_eos__payer;
MP_QSTR_eos__permission;
MP_QSTR_eos__proxy;
MP_QSTR_eos__receiver;
MP_QSTR_eos__refund;
MP_QSTR_eos__requirement;
MP_QSTR_eos__sell_ram;
MP_QSTR_eos__sender;
MP_QSTR_eos__sign_transaction;
MP_QSTR_eos__threshold;
MP_QSTR_eos__to;
MP_QSTR_eos__transfer;
MP_QSTR_eos__type;
MP_QSTR_eos__undelegate;
MP_QSTR_eos__unlink_auth;
MP_QSTR_eos__update_auth;
MP_QSTR_eos__vote_for_producers;
MP_QSTR_eos__vote_for_proxy;
MP_QSTR_eos__voter;
MP_QSTR_erase;
MP_QSTR_ethereum__amount_sent;
MP_QSTR_ethereum__contract;
MP_QSTR_ethereum__data_size_template;
MP_QSTR_ethereum__gas_limit;
MP_QSTR_ethereum__gas_price;
MP_QSTR_ethereum__max_gas_price;
MP_QSTR_ethereum__name_and_version;
MP_QSTR_ethereum__new_contract;
MP_QSTR_ethereum__no_message_field;
MP_QSTR_ethereum__priority_fee;
MP_QSTR_ethereum__show_full_array;
MP_QSTR_ethereum__show_full_domain;
MP_QSTR_ethereum__show_full_message;
MP_QSTR_ethereum__show_full_struct;
MP_QSTR_ethereum__sign_eip712;
MP_QSTR_ethereum__title_confirm_data;
MP_QSTR_ethereum__title_confirm_domain;
MP_QSTR_ethereum__title_confirm_message;
MP_QSTR_ethereum__title_confirm_struct;
MP_QSTR_ethereum__title_confirm_typed_data;
MP_QSTR_ethereum__title_signing_address;
MP_QSTR_ethereum__units_template;
MP_QSTR_ethereum__unknown_token;
MP_QSTR_ethereum__valid_signature;
MP_QSTR_experimental_mode__enable;
MP_QSTR_experimental_mode__only_for_dev;
MP_QSTR_experimental_mode__title;
MP_QSTR_extra;
MP_QSTR_fee_amount;
MP_QSTR_fee_label;
MP_QSTR_fee_rate_amount;
MP_QSTR_fee_title;
MP_QSTR_fee_value;
MP_QSTR_fido__already_registered;
MP_QSTR_fido__device_already_registered;
MP_QSTR_fido__device_already_registered_with_template;
MP_QSTR_fido__device_not_registered;
MP_QSTR_fido__does_not_belong;
MP_QSTR_fido__erase_credentials;
MP_QSTR_fido__export_credentials;
MP_QSTR_fido__not_registered;
MP_QSTR_fido__not_registered_with_template;
MP_QSTR_fido__please_enable_pin_protection;
MP_QSTR_fido__title_authenticate;
MP_QSTR_fido__title_import_credential;
MP_QSTR_fido__title_list_credentials;
MP_QSTR_fido__title_register;
MP_QSTR_fido__title_remove_credential;
MP_QSTR_fido__title_reset;
MP_QSTR_fido__title_u2f_auth;
MP_QSTR_fido__title_u2f_register;
MP_QSTR_fido__title_verify_user;
MP_QSTR_fido__unable_to_verify_user;
MP_QSTR_fido__wanna_erase_credentials;
MP_QSTR_fingerprint;
MP_QSTR_firmware_update__title;
MP_QSTR_firmware_update__title_fingerprint;
MP_QSTR_get_language;
MP_QSTR_hold;
MP_QSTR_hold_danger;
MP_QSTR_homescreen__click_to_connect;
MP_QSTR_homescreen__click_to_unlock;
MP_QSTR_homescreen__title_backup_failed;
MP_QSTR_homescreen__title_backup_needed;
MP_QSTR_homescreen__title_coinjoin_authorized;
MP_QSTR_homescreen__title_experimental_mode;
MP_QSTR_homescreen__title_no_usb_connection;
MP_QSTR_homescreen__title_pin_not_set;
MP_QSTR_homescreen__title_seedless;
MP_QSTR_homescreen__title_set;
MP_QSTR_horizontal;
MP_QSTR_icon_name;
MP_QSTR_image;
MP_QSTR_indeterminate;
MP_QSTR_info_button;
MP_QSTR_init;
MP_QSTR_inputs__back;
MP_QSTR_inputs__cancel;
MP_QSTR_inputs__delete;
MP_QSTR_inputs__enter;
MP_QSTR_inputs__return;
MP_QSTR_inputs__show;
MP_QSTR_inputs__space;
MP_QSTR_is_type_of;
MP_QSTR_items;
MP_QSTR_joint__title;
MP_QSTR_joint__to_the_total_amount;
MP_QSTR_joint__you_are_contributing;
MP_QSTR_label;
MP_QSTR_language;
MP_QSTR_language__change_to;
MP_QSTR_language__changed;
MP_QSTR_language__progress;
MP_QSTR_language__title;
MP_QSTR_lines;
MP_QSTR_load_from_flash;
MP_QSTR_lockscreen__tap_to_connect;
MP_QSTR_lockscreen__tap_to_unlock;
MP_QSTR_lockscreen__title_locked;
MP_QSTR_lockscreen__title_not_connected;
MP_QSTR_max_count;
MP_QSTR_max_feerate;
MP_QSTR_max_len;
MP_QSTR_max_rounds;
MP_QSTR_min_count;
MP_QSTR_misc__decrypt_value;
MP_QSTR_misc__encrypt_value;
MP_QSTR_misc__title_suite_labeling;
MP_QSTR_modify_amount__decrease_amount;
MP_QSTR_modify_amount__increase_amount;
MP_QSTR_modify_amount__new_amount;
MP_QSTR_modify_amount__title;
MP_QSTR_modify_fee__decrease_fee;
MP_QSTR_modify_fee__fee_rate;
MP_QSTR_modify_fee__increase_fee;
MP_QSTR_modify_fee__new_transaction_fee;
MP_QSTR_modify_fee__no_change;
MP_QSTR_modify_fee__title;
MP_QSTR_modify_fee__transaction_fee;
MP_QSTR_monero__confirm_export;
MP_QSTR_monero__confirm_ki_sync;
MP_QSTR_monero__confirm_refresh;
MP_QSTR_monero__confirm_unlock_time;
MP_QSTR_monero__hashing_inputs;
MP_QSTR_monero__payment_id;
MP_QSTR_monero__postprocessing;
MP_QSTR_monero__processing;
MP_QSTR_monero__processing_inputs;
MP_QSTR_monero__processing_outputs;
MP_QSTR_monero__signing;
MP_QSTR_monero__signing_inputs;
MP_QSTR_monero__unlock_time_set_template;
MP_QSTR_monero__wanna_export_tx_der;
MP_QSTR_monero__wanna_export_tx_key;
MP_QSTR_monero__wanna_export_watchkey;
MP_QSTR_monero__wanna_start_refresh;
MP_QSTR_monero__wanna_sync_key_images;
MP_QSTR_multiple_pages_texts;
MP_QSTR_nem__absolute;
MP_QSTR_nem__activate;
MP_QSTR_nem__add;
MP_QSTR_nem__confirm_action;
MP_QSTR_nem__confirm_address;
MP_QSTR_nem__confirm_creation_fee;
MP_QSTR_nem__confirm_mosaic;
MP_QSTR_nem__confirm_multisig_fee;
MP_QSTR_nem__confirm_namespace;
MP_QSTR_nem__confirm_payload;
MP_QSTR_nem__confirm_properties;
MP_QSTR_nem__confirm_rental_fee;
MP_QSTR_nem__confirm_transfer_of;
MP_QSTR_nem__convert_account_to_multisig;
MP_QSTR_nem__cosign_transaction_for;
MP_QSTR_nem__cosignatory;
MP_QSTR_nem__create_mosaic;
MP_QSTR_nem__create_namespace;
MP_QSTR_nem__deactivate;
MP_QSTR_nem__decrease;
MP_QSTR_nem__description;
MP_QSTR_nem__divisibility_and_levy_cannot_be_shown;
MP_QSTR_nem__encrypted;
MP_QSTR_nem__final_confirm;
MP_QSTR_nem__immutable;
MP_QSTR_nem__increase;
MP_QSTR_nem__initial_supply;
MP_QSTR_nem__initiate_transaction_for;
MP_QSTR_nem__levy_divisibility;
MP_QSTR_nem__levy_fee;
MP_QSTR_nem__levy_fee_of;
MP_QSTR_nem__levy_mosaic;
MP_QSTR_nem__levy_namespace;
MP_QSTR_nem__levy_recipient;
MP_QSTR_nem__levy_type;
MP_QSTR_nem__modify_supply_for;
MP_QSTR_nem__modify_the_number_of_cosignatories_by;
MP_QSTR_nem__mutable;
MP_QSTR_nem__of;
MP_QSTR_nem__percentile;
MP_QSTR_nem__raw_units_template;
MP_QSTR_nem__remote_harvesting;
MP_QSTR_nem__remove;
MP_QSTR_nem__set_minimum_cosignatories_to;
MP_QSTR_nem__sign_tx_fee_template;
MP_QSTR_nem__supply_change;
MP_QSTR_nem__supply_units_template;
MP_QSTR_nem__transferable;
MP_QSTR_nem__under_namespace;
MP_QSTR_nem__unencrypted;
MP_QSTR_nem__unknown_mosaic;
MP_QSTR_notification;
MP_QSTR_notification_level;
MP_QSTR_page_count;
MP_QSTR_pages;
MP_QSTR_paint;
MP_QSTR_passphrase__access_hidden_wallet;
MP_QSTR_passphrase__always_on_device;
MP_QSTR_passphrase__from_host_not_shown;
MP_QSTR_passphrase__hidden_wallet;
MP_QSTR_passphrase__hide;
MP_QSTR_passphrase__next_screen_will_show_passphrase;
MP_QSTR_passphrase__please_enter;
MP_QSTR_passphrase__revoke_on_device;
MP_QSTR_passphrase__title_confirm;
MP_QSTR_passphrase__title_enter;
MP_QSTR_passphrase__title_hide;
MP_QSTR_passphrase__title_settings;
MP_QSTR_passphrase__title_source;
MP_QSTR_passphrase__turn_off;
MP_QSTR_passphrase__turn_on;
MP_QSTR_path;
MP_QSTR_pin__change;
MP_QSTR_pin__changed;
MP_QSTR_pin__cursor_will_change;
MP_QSTR_pin__diff_from_wipe_code;
MP_QSTR_pin__disabled;
MP_QSTR_pin__enabled;
MP_QSTR_pin__enter;
MP_QSTR_pin__enter_new;
MP_QSTR_pin__entered_not_valid;
MP_QSTR_pin__info;
MP_QSTR_pin__invalid_pin;
MP_QSTR_pin__last_attempt;
MP_QSTR_pin__mismatch;
MP_QSTR_pin__pin_mismatch;
MP_QSTR_pin__please_check_again;
MP_QSTR_pin__reenter_new;
MP_QSTR_pin__reenter_to_confirm;
MP_QSTR_pin__should_be_long;
MP_QSTR_pin__title_check_pin;
MP_QSTR_pin__title_settings;
MP_QSTR_pin__title_wrong_pin;
MP_QSTR_pin__tries_left;
MP_QSTR_pin__turn_off;
MP_QSTR_pin__turn_on;
MP_QSTR_pin__wrong_pin;
MP_QSTR_plurals__contains_x_keys;
MP_QSTR_plurals__lock_after_x_hours;
MP_QSTR_plurals__lock_after_x_milliseconds;
MP_QSTR_plurals__lock_after_x_minutes;
MP_QSTR_plurals__lock_after_x_seconds;
MP_QSTR_plurals__sign_x_actions;
MP_QSTR_plurals__transaction_of_x_operations;
MP_QSTR_plurals__x_groups_needed;
MP_QSTR_plurals__x_shares_needed;
MP_QSTR_progress__authenticity_check;
MP_QSTR_progress__done;
MP_QSTR_progress__loading_transaction;
MP_QSTR_progress__locking_device;
MP_QSTR_progress__one_second_left;
MP_QSTR_progress__please_wait;
MP_QSTR_progress__processing;
MP_QSTR_progress__refreshing;
MP_QSTR_progress__signing_transaction;
MP_QSTR_progress__syncing;
MP_QSTR_progress__x_seconds_left_template;
MP_QSTR_progress_event;
MP_QSTR_prompt;
MP_QSTR_qr_title;
MP_QSTR_reboot_to_bootloader__just_a_moment;
MP_QSTR_reboot_to_bootloader__restart;
MP_QSTR_reboot_to_bootloader__title;
MP_QSTR_reboot_to_bootloader__version_by_template;
MP_QSTR_recovery__cancel_dry_run;
MP_QSTR_recovery__check_dry_run;
MP_QSTR_recovery__cursor_will_change;
MP_QSTR_recovery__dry_run_bip39_valid_match;
MP_QSTR_recovery__dry_run_bip39_valid_mismatch;
MP_QSTR_recovery__dry_run_slip39_valid_match;
MP_QSTR_recovery__dry_run_slip39_valid_mismatch;
MP_QSTR_recovery__enter_any_share;
MP_QSTR_recovery__enter_backup;
MP_QSTR_recovery__enter_different_share;
MP_QSTR_recovery__enter_share_from_diff_group;
MP_QSTR_recovery__group_num_template;
MP_QSTR_recovery__group_threshold_reached;
MP_QSTR_recovery__invalid_seed_entered;
MP_QSTR_recovery__invalid_share_entered;
MP_QSTR_recovery__more_shares_needed;
MP_QSTR_recovery__num_of_words;
MP_QSTR_recovery__only_first_n_letters;
MP_QSTR_recovery__progress_will_be_lost;
MP_QSTR_recovery__select_num_of_words;
MP_QSTR_recovery__share_already_entered;
MP_QSTR_recovery__share_from_another_shamir;
MP_QSTR_recovery__share_num_template;
MP_QSTR_recovery__title;
MP_QSTR_recovery__title_cancel_dry_run;
MP_QSTR_recovery__title_cancel_recovery;
MP_QSTR_recovery__title_dry_run;
MP_QSTR_recovery__title_recover;
MP_QSTR_recovery__title_remaining_shares;
MP_QSTR_recovery__type_word_x_of_y_template;
MP_QSTR_recovery__wallet_recovered;
MP_QSTR_recovery__wanna_cancel_dry_run;
MP_QSTR_recovery__wanna_cancel_recovery;
MP_QSTR_recovery__word_count_template;
MP_QSTR_recovery__word_x_of_y_template;
MP_QSTR_recovery__x_more_items_starting_template_plural;
MP_QSTR_recovery__x_more_shares_needed_template_plural;
MP_QSTR_recovery__x_of_y_entered_template;
MP_QSTR_recovery__you_have_entered;
MP_QSTR_request_bip39;
MP_QSTR_request_complete_repaint;
MP_QSTR_request_number;
MP_QSTR_request_passphrase;
MP_QSTR_request_pin;
MP_QSTR_request_slip39;
MP_QSTR_reset__advanced_group_threshold_info;
MP_QSTR_reset__all_x_of_y_template;
MP_QSTR_reset__any_x_of_y_template;
MP_QSTR_reset__button_create;
MP_QSTR_reset__button_recover;
MP_QSTR_reset__by_continuing;
MP_QSTR_reset__check_backup_title;
MP_QSTR_reset__check_group_share_title_template;
MP_QSTR_reset__check_seed_title;
MP_QSTR_reset__check_share_title_template;
MP_QSTR_reset__continue_with_next_share;
MP_QSTR_reset__continue_with_share_template;
MP_QSTR_reset__finished_verifying_group_template;
MP_QSTR_reset__finished_verifying_seed;
MP_QSTR_reset__finished_verifying_shares;
MP_QSTR_reset__group_description;
MP_QSTR_reset__group_info;
MP_QSTR_reset__group_share_checked_successfully_template;
MP_QSTR_reset__group_share_title_template;
MP_QSTR_reset__more_info_at;
MP_QSTR_reset__need_all_share_template;
MP_QSTR_reset__need_any_share_template;
MP_QSTR_reset__needed_to_form_a_group;
MP_QSTR_reset__needed_to_recover_your_wallet;
MP_QSTR_reset__never_make_digital_copy;
MP_QSTR_reset__num_of_share_holders_template;
MP_QSTR_reset__num_of_shares_advanced_info_template;
MP_QSTR_reset__num_of_shares_basic_info;
MP_QSTR_reset__num_shares_for_group_template;
MP_QSTR_reset__number_of_shares_info;
MP_QSTR_reset__one_share;
MP_QSTR_reset__only_one_share_will_be_created;
MP_QSTR_reset__recovery_seed_title;
MP_QSTR_reset__recovery_share_title_template;
MP_QSTR_reset__required_number_of_groups;
MP_QSTR_reset__select_correct_word;
MP_QSTR_reset__select_word_template;
MP_QSTR_reset__select_word_x_of_y_template;
MP_QSTR_reset__set_it_to_count_template;
MP_QSTR_reset__share_checked_successfully_template;
MP_QSTR_reset__share_words_title;
MP_QSTR_reset__slip39_checklist_num_groups;
MP_QSTR_reset__slip39_checklist_num_shares;
MP_QSTR_reset__slip39_checklist_set_num_groups;
MP_QSTR_reset__slip39_checklist_set_num_shares;
MP_QSTR_reset__slip39_checklist_set_sizes;
MP_QSTR_reset__slip39_checklist_set_sizes_longer;
MP_QSTR_reset__slip39_checklist_set_threshold;
MP_QSTR_reset__slip39_checklist_title;
MP_QSTR_reset__slip39_checklist_write_down;
MP_QSTR_reset__slip39_checklist_write_down_recovery;
MP_QSTR_reset__the_threshold_sets_the_number_of_shares;
MP_QSTR_reset__threshold_info;
MP_QSTR_reset__title_backup_is_done;
MP_QSTR_reset__title_create_wallet;
MP_QSTR_reset__title_create_wallet_shamir;
MP_QSTR_reset__title_group_threshold;
MP_QSTR_reset__title_number_of_groups;
MP_QSTR_reset__title_number_of_shares;
MP_QSTR_reset__title_set_group_threshold;
MP_QSTR_reset__title_set_number_of_groups;
MP_QSTR_reset__title_set_number_of_shares;
MP_QSTR_reset__title_set_threshold;
MP_QSTR_reset__to_form_group_template;
MP_QSTR_reset__tos_link;
MP_QSTR_reset__total_number_of_shares_in_group_template;
MP_QSTR_reset__use_your_backup;
MP_QSTR_reset__write_down_words_template;
MP_QSTR_reset__wrong_word_selected;
MP_QSTR_reset__you_need_one_share;
MP_QSTR_reset__your_backup_is_done;
MP_QSTR_reverse;
MP_QSTR_ripple__confirm_tag;
MP_QSTR_ripple__destination_tag_template;
MP_QSTR_rotation__change_template;
MP_QSTR_rotation__east;
MP_QSTR_rotation__north;
MP_QSTR_rotation__south;
MP_QSTR_rotation__title_change;
MP_QSTR_rotation__west;
MP_QSTR_safety_checks__approve_unsafe_always;
MP_QSTR_safety_checks__approve_unsafe_temporary;
MP_QSTR_safety_checks__enforce_strict;
MP_QSTR_safety_checks__title;
MP_QSTR_safety_checks__title_safety_override;
MP_QSTR_sd_card__all_data_will_be_lost;
MP_QSTR_sd_card__card_required;
MP_QSTR_sd_card__disable;
MP_QSTR_sd_card__disabled;
MP_QSTR_sd_card__enable;
MP_QSTR_sd_card__enabled;
MP_QSTR_sd_card__error;
MP_QSTR_sd_card__format_card;
MP_QSTR_sd_card__insert_correct_card;
MP_QSTR_sd_card__please_insert;
MP_QSTR_sd_card__please_unplug_and_insert;
MP_QSTR_sd_card__problem_accessing;
MP_QSTR_sd_card__refresh;
MP_QSTR_sd_card__refreshed;
MP_QSTR_sd_card__restart;
MP_QSTR_sd_card__title;
MP_QSTR_sd_card__title_problem;
MP_QSTR_sd_card__unknown_filesystem;
MP_QSTR_sd_card__unplug_and_insert_correct;
MP_QSTR_sd_card__use_different_card;
MP_QSTR_sd_card__wanna_format;
MP_QSTR_sd_card__wrong_sd_card;
MP_QSTR_select_word;
MP_QSTR_select_word_count;
MP_QSTR_send__address_path;
MP_QSTR_send__confirm_sending;
MP_QSTR_send__from_multiple_accounts;
MP_QSTR_send__including_fee;
MP_QSTR_send__maximum_fee;
MP_QSTR_send__receiving_to_multisig;
MP_QSTR_send__title_confirm_sending;
MP_QSTR_send__title_joint_transaction;
MP_QSTR_send__title_receiving_to;
MP_QSTR_send__title_sending;
MP_QSTR_send__title_sending_amount;
MP_QSTR_send__title_sending_to;
MP_QSTR_send__to_the_total_amount;
MP_QSTR_send__total_amount;
MP_QSTR_send__transaction_id;
MP_QSTR_send__you_are_contributing;
MP_QSTR_share_words;
MP_QSTR_share_words__words_in_order;
MP_QSTR_share_words__wrote_down_all;
MP_QSTR_show_address_details;
MP_QSTR_show_checklist;
MP_QSTR_show_error;
@ -131,32 +802,211 @@ static void _librust_qstrs(void) {
MP_QSTR_show_share_words;
MP_QSTR_show_simple;
MP_QSTR_show_success;
MP_QSTR_show_wait_text;
MP_QSTR_show_warning;
MP_QSTR_sign;
MP_QSTR_sign_message__bytes_template;
MP_QSTR_sign_message__confirm_address;
MP_QSTR_sign_message__confirm_message;
MP_QSTR_sign_message__message_size;
MP_QSTR_sign_message__verify_address;
MP_QSTR_skip_first_paint;
MP_QSTR_solana__account_index;
MP_QSTR_solana__associated_token_account;
MP_QSTR_solana__confirm_multisig;
MP_QSTR_solana__expected_fee;
MP_QSTR_solana__instruction_accounts_template;
MP_QSTR_solana__instruction_data;
MP_QSTR_solana__instruction_is_multisig;
MP_QSTR_solana__is_provided_via_lookup_table_template;
MP_QSTR_solana__lookup_table_address;
MP_QSTR_solana__multiple_signers;
MP_QSTR_solana__token_address;
MP_QSTR_solana__transaction_contains_unknown_instructions;
MP_QSTR_solana__transaction_requires_x_signers_template;
MP_QSTR_spending_amount;
MP_QSTR_stellar__account_merge;
MP_QSTR_stellar__account_thresholds;
MP_QSTR_stellar__add_signer;
MP_QSTR_stellar__add_trust;
MP_QSTR_stellar__all_will_be_sent_to;
MP_QSTR_stellar__allow_trust;
MP_QSTR_stellar__asset;
MP_QSTR_stellar__balance_id;
MP_QSTR_stellar__bump_sequence;
MP_QSTR_stellar__buying;
MP_QSTR_stellar__claim_claimable_balance;
MP_QSTR_stellar__clear_data;
MP_QSTR_stellar__clear_flags;
MP_QSTR_stellar__confirm_issuer;
MP_QSTR_stellar__confirm_memo;
MP_QSTR_stellar__confirm_network;
MP_QSTR_stellar__confirm_operation;
MP_QSTR_stellar__confirm_stellar;
MP_QSTR_stellar__confirm_timebounds;
MP_QSTR_stellar__create_account;
MP_QSTR_stellar__debited_amount;
MP_QSTR_stellar__delete;
MP_QSTR_stellar__delete_passive_offer;
MP_QSTR_stellar__delete_trust;
MP_QSTR_stellar__destination;
MP_QSTR_stellar__exchanges_require_memo;
MP_QSTR_stellar__final_confirm;
MP_QSTR_stellar__hash;
MP_QSTR_stellar__high;
MP_QSTR_stellar__home_domain;
MP_QSTR_stellar__inflation;
MP_QSTR_stellar__initial_balance;
MP_QSTR_stellar__initialize_signing_with;
MP_QSTR_stellar__issuer_template;
MP_QSTR_stellar__key;
MP_QSTR_stellar__limit;
MP_QSTR_stellar__low;
MP_QSTR_stellar__master_weight;
MP_QSTR_stellar__medium;
MP_QSTR_stellar__new_offer;
MP_QSTR_stellar__new_passive_offer;
MP_QSTR_stellar__no_memo_set;
MP_QSTR_stellar__no_restriction;
MP_QSTR_stellar__on_network_template;
MP_QSTR_stellar__path_pay;
MP_QSTR_stellar__path_pay_at_least;
MP_QSTR_stellar__pay;
MP_QSTR_stellar__pay_at_most;
MP_QSTR_stellar__preauth_transaction;
MP_QSTR_stellar__price_per_template;
MP_QSTR_stellar__private_network;
MP_QSTR_stellar__remove_signer;
MP_QSTR_stellar__revoke_trust;
MP_QSTR_stellar__selling;
MP_QSTR_stellar__set_data;
MP_QSTR_stellar__set_flags;
MP_QSTR_stellar__set_sequence_to_template;
MP_QSTR_stellar__sign_tx_count_template;
MP_QSTR_stellar__sign_tx_fee_template;
MP_QSTR_stellar__source_account;
MP_QSTR_stellar__testnet_network;
MP_QSTR_stellar__trusted_account;
MP_QSTR_stellar__update;
MP_QSTR_stellar__valid_from;
MP_QSTR_stellar__valid_to;
MP_QSTR_stellar__value_sha256;
MP_QSTR_stellar__wanna_clean_value_key_template;
MP_QSTR_stellar__your_account;
MP_QSTR_subprompt;
MP_QSTR_subtitle;
MP_QSTR_tezos__baker_address;
MP_QSTR_tezos__balance;
MP_QSTR_tezos__ballot;
MP_QSTR_tezos__confirm_delegation;
MP_QSTR_tezos__confirm_origination;
MP_QSTR_tezos__delegator;
MP_QSTR_tezos__proposal;
MP_QSTR_tezos__register_delegate;
MP_QSTR_tezos__remove_delegation;
MP_QSTR_tezos__submit_ballot;
MP_QSTR_tezos__submit_proposal;
MP_QSTR_tezos__submit_proposals;
MP_QSTR_time_ms;
MP_QSTR_timer;
MP_QSTR_title;
MP_QSTR_total_amount;
MP_QSTR_total_fee_new;
MP_QSTR_total_label;
MP_QSTR_total_len;
MP_QSTR_touch_event;
MP_QSTR_trace;
MP_QSTR_trezorproto;
MP_QSTR_trezorui2;
MP_QSTR_tutorial;
MP_QSTR_tutorial__middle_click;
MP_QSTR_tutorial__press_and_hold;
MP_QSTR_tutorial__ready_to_use;
MP_QSTR_tutorial__scroll_down;
MP_QSTR_tutorial__sure_you_want_skip;
MP_QSTR_tutorial__title_hello;
MP_QSTR_tutorial__title_screen_scroll;
MP_QSTR_tutorial__title_skip;
MP_QSTR_tutorial__title_tutorial_complete;
MP_QSTR_tutorial__use_trezor;
MP_QSTR_tutorial__welcome_press_right;
MP_QSTR_type_for_name;
MP_QSTR_type_for_wire;
MP_QSTR_u2f__get;
MP_QSTR_u2f__set_template;
MP_QSTR_u2f__title_get;
MP_QSTR_u2f__title_set;
MP_QSTR_usb_event;
MP_QSTR_user_fee_change;
MP_QSTR_value;
MP_QSTR_verb;
MP_QSTR_verb_cancel;
MP_QSTR_verify;
MP_QSTR_version;
MP_QSTR_warning;
MP_QSTR_wipe__info;
MP_QSTR_wipe__title;
MP_QSTR_wipe__want_to_wipe;
MP_QSTR_wipe_code__change;
MP_QSTR_wipe_code__changed;
MP_QSTR_wipe_code__diff_from_pin;
MP_QSTR_wipe_code__disabled;
MP_QSTR_wipe_code__enabled;
MP_QSTR_wipe_code__enter_new;
MP_QSTR_wipe_code__info;
MP_QSTR_wipe_code__invalid;
MP_QSTR_wipe_code__mismatch;
MP_QSTR_wipe_code__reenter;
MP_QSTR_wipe_code__reenter_to_confirm;
MP_QSTR_wipe_code__title_check;
MP_QSTR_wipe_code__title_invalid;
MP_QSTR_wipe_code__title_settings;
MP_QSTR_wipe_code__turn_off;
MP_QSTR_wipe_code__turn_on;
MP_QSTR_wipe_code__wipe_code_mismatch;
MP_QSTR_word_count__title;
MP_QSTR_words;
MP_QSTR_words__account;
MP_QSTR_words__account_colon;
MP_QSTR_words__address;
MP_QSTR_words__amount;
MP_QSTR_words__are_you_sure;
MP_QSTR_words__array_of;
MP_QSTR_words__blockhash;
MP_QSTR_words__buying;
MP_QSTR_words__confirm;
MP_QSTR_words__confirm_fee;
MP_QSTR_words__contains;
MP_QSTR_words__continue_anyway;
MP_QSTR_words__continue_with;
MP_QSTR_words__error;
MP_QSTR_words__fee;
MP_QSTR_words__from;
MP_QSTR_words__keep_it_safe;
MP_QSTR_words__know_what_your_doing;
MP_QSTR_words__my_trezor;
MP_QSTR_words__no;
MP_QSTR_words__outputs;
MP_QSTR_words__please_check_again;
MP_QSTR_words__please_try_again;
MP_QSTR_words__really_wanna;
MP_QSTR_words__recipient;
MP_QSTR_words__sign;
MP_QSTR_words__signer;
MP_QSTR_words__title_check;
MP_QSTR_words__title_group;
MP_QSTR_words__title_information;
MP_QSTR_words__title_remember;
MP_QSTR_words__title_share;
MP_QSTR_words__title_shares;
MP_QSTR_words__title_success;
MP_QSTR_words__title_summary;
MP_QSTR_words__title_threshold;
MP_QSTR_words__unknown;
MP_QSTR_words__warning;
MP_QSTR_words__writable;
MP_QSTR_words__yes;
MP_QSTR_write;
MP_QSTR_wrong_pin;
MP_QSTR_xpubs;
}

@ -27,6 +27,8 @@ mod storage;
mod time;
#[cfg(feature = "ui_debug")]
mod trace;
#[cfg(feature = "translations")]
pub mod translations;
#[cfg(feature = "ui")]
#[macro_use]

@ -24,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(Debug, Clone)]
#[derive(Debug, Copy, Clone)]
pub struct StrBuffer {
ptr: *const u8,
len: u16,

@ -137,8 +137,9 @@ macro_rules! obj_dict {
macro_rules! obj_type {
(name: $name:expr,
$(locals: $locals:expr,)?
$(attr_fn: $attr_fn:ident,)?
$(call_fn: $call_fn:ident,)?
$(make_new_fn: $make_new_fn:path,)?
$(attr_fn: $attr_fn:path,)?
$(call_fn: $call_fn:path,)?
) => {{
#[allow(unused_unsafe)]
unsafe {
@ -156,6 +157,11 @@ macro_rules! obj_type {
let mut call: ffi::mp_call_fun_t = None;
$(call = Some($call_fn);)?
#[allow(unused_mut)]
#[allow(unused_assignments)]
let mut make_new: ffi::mp_make_new_fun_t = None;
$(make_new = Some($make_new_fn);)?
// TODO: This is safe only if we pass in `Dict` with fixed `Map` (created by
// `Map::fixed()`, usually through `obj_map!`), because only then will
// MicroPython treat `locals_dict` as immutable, and make the mutable cast safe.
@ -171,7 +177,7 @@ macro_rules! obj_type {
flags: 0,
name,
print: None,
make_new: None,
make_new,
call,
unary_op: None,
binary_op: None,

@ -15,7 +15,7 @@ impl Qstr {
// micropython/py/obj.h #define MP_OBJ_NEW_QSTR(qst)
// ((mp_obj_t)((((mp_uint_t)(qst)) << 3) | 2))
let bits = (self.0 << 3) | 2;
unsafe { Obj::from_bits(bits as usize) }
unsafe { Obj::from_bits(bits) }
}
pub const fn from_obj_bits(bits: cty::uintptr_t) -> Self {

@ -17,6 +17,14 @@ impl Type {
ObjBase { type_: self }
}
pub const fn as_obj(&'static self) -> Obj {
// SAFETY:
// - We are an object struct with a base and a type.
// - 'static lifetime holds us in place.
// - MicroPython is smart enough not to mutate `mp_obj_type_t` objects.
unsafe { Obj::from_ptr(self as *const _ as *mut _) }
}
#[cfg(feature = "debug")]
pub fn name(&self) -> &'static str {
use super::qstr::Qstr;

@ -35,6 +35,7 @@ const SD_SALT_AUTH_KEY: u16 = FLAG_PUBLIC | APP_DEVICE | 0x0012;
const INITIALIZED: u16 = FLAG_PUBLIC | APP_DEVICE | 0x0013;
const SAFETY_CHECK_LEVEL: u16 = APP_DEVICE | 0x0014;
const EXPERIMENTAL_FEATURES: u16 = APP_DEVICE | 0x0015;
const HIDE_PASSPHRASE_FROM_HOST: u16 = APP_DEVICE | 0x0016;
pub fn get_avatar_len() -> StorageResult<usize> {
get_length(HOMESCREEN)

@ -1,5 +1,14 @@
use heapless::String;
#[cfg(feature = "micropython")]
use crate::error::Error;
#[cfg(feature = "micropython")]
use crate::micropython::{buffer::StrBuffer, obj::Obj};
#[cfg(feature = "translations")]
use crate::translations::TR;
/// Trait for slicing off string prefix by a specified number of bytes.
/// See `StringType` for deeper explanation.
pub trait SkipPrefix {
@ -23,9 +32,15 @@ impl SkipPrefix for &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 {}
pub trait StringType:
AsRef<str> + From<&'static str> + Into<TString<'static>> + SkipPrefix
{
}
impl<T> StringType for T where T: AsRef<str> + From<&'static str> + SkipPrefix {}
impl<T> StringType for T where
T: AsRef<str> + From<&'static str> + Into<TString<'static>> + SkipPrefix
{
}
/// Unified-length String type, long enough for most simple use-cases.
pub type ShortString = String<50>;
@ -72,3 +87,92 @@ pub fn format_i64(num: i64, buffer: &mut [u8]) -> Option<&str> {
}
}
}
#[derive(Copy, Clone)]
pub enum TString<'a> {
#[cfg(feature = "micropython")]
Allocated(StrBuffer),
#[cfg(feature = "translations")]
Translation(TR),
Str(&'a str),
}
impl TString<'_> {
pub fn is_empty(&self) -> bool {
self.map(|s| s.is_empty())
}
pub fn map<F, T>(&self, fun: F) -> T
where
F: for<'a> FnOnce(&'a str) -> T,
T: 'static,
{
match self {
#[cfg(feature = "micropython")]
Self::Allocated(buf) => fun(buf.as_ref()),
#[cfg(feature = "translations")]
Self::Translation(tr) => tr.map_translated(fun),
Self::Str(s) => fun(s),
}
}
}
impl TString<'static> {
#[cfg(feature = "translations")]
pub const fn from_translation(tr: TR) -> Self {
Self::Translation(tr)
}
#[cfg(feature = "micropython")]
pub const fn from_strbuffer(buf: StrBuffer) -> Self {
Self::Allocated(buf)
}
pub const fn empty() -> Self {
Self::Str("")
}
}
impl<'a> TString<'a> {
pub const fn from_str(s: &'a str) -> Self {
Self::Str(s)
}
}
impl<'a> From<&'a str> for TString<'a> {
fn from(s: &'a str) -> Self {
Self::Str(s)
}
}
#[cfg(feature = "translations")]
impl From<TR> for TString<'static> {
fn from(tr: TR) -> Self {
Self::from_translation(tr)
}
}
#[cfg(feature = "micropython")]
impl From<StrBuffer> for TString<'static> {
fn from(buf: StrBuffer) -> Self {
Self::from_strbuffer(buf)
}
}
#[cfg(feature = "micropython")]
impl TryFrom<Obj> for TString<'static> {
type Error = Error;
fn try_from(obj: Obj) -> Result<Self, Self::Error> {
Ok(StrBuffer::try_from(obj)?.into())
}
}
#[cfg(feature = "micropython")]
impl<'a> TryFrom<TString<'a>> for Obj {
type Error = Error;
fn try_from(s: TString<'a>) -> Result<Self, Self::Error> {
s.map(|t| t.try_into())
}
}

@ -1,9 +1,9 @@
use crate::strutil::format_i64;
use crate::strutil::{format_i64, TString};
pub trait Tracer {
fn child(&mut self, key: &str, value: &dyn Trace);
fn int(&mut self, key: &str, i: i64);
fn string(&mut self, key: &str, s: &str);
fn string(&mut self, key: &str, s: TString<'_>);
fn bool(&mut self, key: &str, b: bool);
fn null(&mut self, key: &str);
@ -11,14 +11,14 @@ pub trait Tracer {
fn in_list(&mut self, key: &str, block: &dyn Fn(&mut dyn ListTracer));
fn component(&mut self, name: &str) {
self.string("component", name);
self.string("component", name.into());
}
}
pub trait ListTracer {
fn child(&mut self, value: &dyn Trace);
fn int(&mut self, i: i64);
fn string(&mut self, s: &str);
fn string(&mut self, s: &TString<'_>);
fn bool(&mut self, b: bool);
fn in_child(&mut self, block: &dyn Fn(&mut dyn Tracer));
@ -121,9 +121,9 @@ impl<F: FnMut(&str)> ListTracer for JsonTracer<F> {
self.write_int(i);
}
fn string(&mut self, s: &str) {
fn string(&mut self, s: &TString<'_>) {
self.maybe_comma();
self.write_str_quoted(s);
s.map(|s| self.write_str_quoted(s));
}
fn bool(&mut self, b: bool) {
@ -160,9 +160,9 @@ impl<F: FnMut(&str)> Tracer for JsonTracer<F> {
self.write_int(i);
}
fn string(&mut self, key: &str, s: &str) {
fn string(&mut self, key: &str, s: TString<'_>) {
self.key(key);
self.write_str_quoted(s);
s.map(|s| self.write_str_quoted(s));
}
fn bool(&mut self, key: &str, b: bool) {

@ -0,0 +1,386 @@
use core::{mem, str};
use crate::{
crypto::{cosi, ed25519, merkle::merkle_root, sha256},
error::Error,
io::InputStream,
};
use super::public_keys;
pub const MAX_HEADER_LEN: u16 = 1024;
pub const EMPTY_BYTE: u8 = 0xFF;
const SENTINEL_ID: u16 = 0xFFFF;
const SIGNATURE_THRESHOLD: u8 = 2;
// Maximum padding at the end of an offsets table (typically for alignment
// purposes). We allow at most 3 for alignment 4. In practice right now this
// should be max 1.
const MAX_TABLE_PADDING: usize = 3;
const INVALID_TRANSLATIONS_BLOB: Error = value_error!("Invalid translations blob");
#[repr(packed)]
struct OffsetEntry {
pub id: u16,
pub offset: u16,
}
pub struct Table<'a> {
offsets: &'a [OffsetEntry],
data: &'a [u8],
}
fn validate_offset_table(
data_len: usize,
mut iter: impl Iterator<Item = u16>,
) -> Result<(), Error> {
// every offset table must have at least the sentinel
let mut prev = iter.next().ok_or(INVALID_TRANSLATIONS_BLOB)?;
if prev != 0 {
// first offset must always be 0 (even as a sentinel, indicating no data)
return Err(INVALID_TRANSLATIONS_BLOB);
}
for next in iter {
// offsets must be in ascending order
if prev > next {
return Err(INVALID_TRANSLATIONS_BLOB);
}
prev = next;
}
// sentinel needs to be at least data_len - MAX_TABLE_PADDING, and at most
// data_len
let sentinel = prev as usize;
if sentinel < data_len - MAX_TABLE_PADDING || sentinel > data_len {
return Err(INVALID_TRANSLATIONS_BLOB);
}
Ok(())
}
impl<'a> Table<'a> {
pub fn new(mut reader: InputStream<'a>) -> Result<Self, Error> {
let count = reader.read_u16_le()?;
// The offsets table is (count + 1) entries long, the last entry is a sentinel.
let offsets_data = reader.read((count + 1) as usize * mem::size_of::<OffsetEntry>())?;
// SAFETY: OffsetEntry is repr(packed) of two u16 values, so any four bytes are
// a valid OffsetEntry value.
let (_prefix, offsets, _suffix) = unsafe { offsets_data.align_to::<OffsetEntry>() };
if !_prefix.is_empty() || !_suffix.is_empty() {
return Err(INVALID_TRANSLATIONS_BLOB);
}
Ok(Self {
offsets,
data: reader.rest(),
})
}
pub fn validate(&self) -> Result<(), Error> {
validate_offset_table(self.data.len(), self.offsets.iter().map(|it| it.offset))?;
if !matches!(
self.offsets.iter().last().map(|it| it.id),
Some(SENTINEL_ID)
) {
return Err(INVALID_TRANSLATIONS_BLOB);
}
// check that the ids are sorted
let Some(first_entry) = self.offsets.first() else {
// empty table is sorted
return Ok(());
};
let mut prev_id = first_entry.id;
for entry in self.offsets.iter().skip(1) {
if entry.id <= prev_id {
return Err(INVALID_TRANSLATIONS_BLOB);
}
prev_id = entry.id;
}
Ok(())
}
pub fn get(&self, id: u16) -> Option<&'a [u8]> {
self.offsets
.binary_search_by_key(&id, |it| it.id)
.ok()
.and_then(|idx| {
let start = self.offsets[idx].offset as usize;
let end = self.offsets[idx + 1].offset as usize;
self.data.get(start..end)
})
}
pub fn iter(&self) -> impl Iterator<Item = (u16, &'a [u8])> + '_ {
let mut prev_offset = 0usize;
self.offsets.iter().skip(1).map(move |entry| {
let start = prev_offset;
let end = entry.offset as usize;
prev_offset = end;
(entry.id, &self.data[start..end])
})
}
}
pub struct Translations<'a> {
pub header: TranslationsHeader<'a>,
translations: &'a [u8],
translations_offsets: &'a [u16],
fonts: Table<'a>,
}
fn read_u16_prefixed_block<'a>(reader: &mut InputStream<'a>) -> Result<InputStream<'a>, Error> {
let len = reader.read_u16_le()? as usize;
reader.read_stream(len)
}
impl<'a> Translations<'a> {
const MAGIC: &'static [u8] = b"TRTR00";
pub fn new(blob: &'a [u8]) -> Result<Self, Error> {
let mut blob_reader = InputStream::new(blob);
let (header, payload_reader) = TranslationsHeader::parse_from(&mut blob_reader)?;
// validate that the trailing bytes, if any, are empty
let remaining = blob_reader.rest();
if !remaining.iter().all(|&b| b == EMPTY_BYTE) {
// TODO optimize to quadwords?
return Err(value_error!("Trailing data in translations blob"));
}
let payload_bytes = payload_reader.rest();
let payload_digest = sha256::digest(payload_bytes);
if payload_digest != header.data_hash {
return Err(INVALID_TRANSLATIONS_BLOB);
}
let mut payload_reader = InputStream::new(payload_bytes);
let mut translations_reader = read_u16_prefixed_block(&mut payload_reader)?;
let fonts_reader = read_u16_prefixed_block(&mut payload_reader)?;
if payload_reader.remaining() > 0 {
return Err(INVALID_TRANSLATIONS_BLOB);
}
// construct translations data
let translations_count = translations_reader.read_u16_le()? as usize;
let translations_offsets_bytes =
translations_reader.read((translations_count + 1) * mem::size_of::<u16>())?;
// SAFETY: any bytes are valid u16 values, so casting any data to
// a sequence of u16 values is safe.
let (_prefix, translations_offsets, _suffix) =
unsafe { translations_offsets_bytes.align_to::<u16>() };
if !_prefix.is_empty() || !_suffix.is_empty() {
return Err(INVALID_TRANSLATIONS_BLOB);
}
let translations = translations_reader.rest();
validate_offset_table(translations.len(), translations_offsets.iter().copied())?;
// construct and validate font table
let fonts = Table::new(fonts_reader)?;
fonts.validate()?;
for (_, font_data) in fonts.iter() {
let reader = InputStream::new(font_data);
let font_table = Table::new(reader)?;
font_table.validate()?;
}
Ok(Self {
header,
translations,
translations_offsets,
fonts,
})
}
/// Returns the translation at the given index.
pub fn translation(&self, index: usize) -> Option<&str> {
if index + 1 >= self.translations_offsets.len() {
// The index is out of bounds.
// (The last entry is a sentinel, so the last valid index is len - 2)
// May happen when new firmware is using older translations and the string
// is not defined yet.
// Fallback to english.
return None;
}
let start_offset = self.translations_offsets[index] as usize;
let end_offset = self.translations_offsets[index + 1] as usize;
// Construct the relevant slice
let string = &self.translations[start_offset..end_offset];
if string.is_empty() {
// The string is not defined in the blob.
// May happen when old firmware is using newer translations and the string
// was deleted in the newer version.
// Fallback to english.
return None;
}
str::from_utf8(string).ok()
}
pub fn font(&'a self, index: u16) -> Option<Table<'a>> {
self.fonts
.get(index)
.and_then(|data| Table::new(InputStream::new(data)).ok())
}
}
pub struct TranslationsHeader<'a> {
/// Raw content of the header, for signature verification
pub header_bytes: &'a [u8],
/// BCP 47 language tag (cs-CZ, en-US, ...)
pub language: &'a str,
/// 4 bytes of version (major, minor, patch, build)
pub version: [u8; 4],
/// Length of the raw data, i.e. translations section + fonts section
pub data_len: usize,
/// Hash of the data blob (excluding the header)
pub data_hash: sha256::Digest,
/// Merkle proof items
pub merkle_proof: &'a [sha256::Digest],
/// CoSi signature
pub signature: cosi::Signature,
/// Expected total length of the blob
pub total_len: usize,
}
fn read_fixedsize_str<'a>(reader: &mut InputStream<'a>, len: usize) -> Result<&'a str, Error> {
let bytes = reader.read(len)?;
let find_zero = bytes.iter().position(|&b| b == 0).unwrap_or(len);
let bytes_trimmed = &bytes[..find_zero];
core::str::from_utf8(bytes_trimmed).map_err(|_| INVALID_TRANSLATIONS_BLOB)
}
impl<'a> TranslationsHeader<'a> {
const BLOB_MAGIC: &'static [u8] = b"TRTR00";
const HEADER_MAGIC: &'static [u8] = b"TR";
const LANGUAGE_TAG_LEN: usize = 8;
/// Parse a translations header out of a stream.
///
/// The returned tuple consists of:
/// (a) the parsed header and
/// (b) reader of the payload section of the translations blob.
/// The caller can use the returned reader to parse the payload.
///
/// The input stream is positioned at the end of the translations blob (or
/// at the end of stream, whichever comes sooner). The caller can use this
/// to verify that there is no unexpected trailing data in the input
/// stream. (Also, you cannot make a mistake and read the payload out of
/// the input stream).
pub fn parse_from(reader: &mut InputStream<'a>) -> Result<(Self, InputStream<'a>), Error> {
//
// 1. parse outer container
//
let magic = reader.read(Self::BLOB_MAGIC.len())?;
if magic != Self::BLOB_MAGIC {
return Err(INVALID_TRANSLATIONS_BLOB);
}
// read length of contained data
let container_length = reader.read_u16_le()? as usize;
// continue working on the contained data (i.e., read beyond the bounds of
// container_length will result in EOF).
let mut reader = reader.read_stream(container_length.min(reader.remaining()))?;
//
// 2. parse the header section
//
let header_bytes = read_u16_prefixed_block(&mut reader)?.rest();
let mut header_reader = InputStream::new(header_bytes);
let magic = header_reader.read(Self::HEADER_MAGIC.len())?;
if magic != Self::HEADER_MAGIC {
return Err(INVALID_TRANSLATIONS_BLOB);
}
let language = read_fixedsize_str(&mut header_reader, Self::LANGUAGE_TAG_LEN)?;
if language.is_empty() {
return Err(INVALID_TRANSLATIONS_BLOB);
}
let model = read_fixedsize_str(&mut header_reader, 4)?;
if model != crate::trezorhal::model::INTERNAL_NAME {
return Err(value_error!("Wrong Trezor model"));
}
let version_bytes = header_reader.read(4)?;
let version = unwrap!(version_bytes.try_into());
let data_len = header_reader.read_u16_le()? as usize;
let data_hash: sha256::Digest =
unwrap!(header_reader.read(sha256::DIGEST_SIZE)?.try_into());
// ignore the rest of the header reader - this allows older firmware to
// understand newer header if there are only added items
_ = header_reader.rest();
//
// 3. parse the proof section
//
let mut proof_reader = read_u16_prefixed_block(&mut reader)?;
let proof_count = proof_reader.read_byte()? as usize;
let proof_length = proof_count * sha256::DIGEST_SIZE;
let proof_bytes = proof_reader.read(proof_length)?;
// create a list of the proof items
// SAFETY: sha256::Digest is a plain array of u8, so any bytes are valid
let (_prefix, merkle_proof, _suffix) = unsafe { proof_bytes.align_to::<sha256::Digest>() };
if !_prefix.is_empty() || !_suffix.is_empty() {
return Err(INVALID_TRANSLATIONS_BLOB);
}
let signature = cosi::Signature::new(
proof_reader.read_byte()?,
unwrap!(proof_reader.read(ed25519::SIGNATURE_SIZE)?.try_into()),
);
// check that there is no trailing data in the proof section
if proof_reader.remaining() > 0 {
return Err(INVALID_TRANSLATIONS_BLOB);
}
// check that the declared data section length matches the container size
if container_length - reader.tell() != data_len {
return Err(INVALID_TRANSLATIONS_BLOB);
}
let new = Self {
header_bytes,
language,
version,
data_len,
data_hash,
merkle_proof,
signature,
total_len: container_length + Self::BLOB_MAGIC.len() + mem::size_of::<u16>(),
};
new.verify()?;
Ok((new, reader))
}
fn verify_with_keys(&self, public_keys: &[ed25519::PublicKey]) -> Result<(), Error> {
let merkle_root = merkle_root(self.header_bytes, self.merkle_proof);
Ok(cosi::verify(
SIGNATURE_THRESHOLD,
&merkle_root,
public_keys,
&self.signature,
)?)
}
pub fn verify(&self) -> Result<(), Error> {
let mut result = self.verify_with_keys(&public_keys::PUBLIC_KEYS);
#[cfg(feature = "debug")]
if result.is_err() {
// allow development keys
result = self.verify_with_keys(&public_keys::PUBLIC_KEYS_DEVEL);
}
result
}
}

@ -0,0 +1,80 @@
use crate::{error::Error, trezorhal::translations};
use super::blob::Translations;
static mut TRANSLATIONS_ON_FLASH: Option<Translations> = None;
pub fn erase() -> Result<(), Error> {
// SAFETY: Looking is safe (in a single threaded environment).
if unsafe { TRANSLATIONS_ON_FLASH.is_some() } {
return Err(value_error!("Translations blob already set"));
}
// SAFETY: The blob is not set, so there are no references to it.
unsafe { translations::erase() };
Ok(())
}
pub fn write(data: &[u8], offset: usize) -> Result<(), Error> {
// SAFETY: Looking is safe (in a single threaded environment).
if unsafe { TRANSLATIONS_ON_FLASH.is_some() } {
return Err(value_error!("Translations blob already set"));
}
// SAFETY: The blob is not set, so there are no references to it.
let result = unsafe { translations::write(data, offset) };
if result {
Ok(())
} else {
Err(value_error!("Failed to write translations blob"))
}
}
/// Load translations from flash, validate, and cache references to lookup
/// tables.
unsafe fn try_init<'a>() -> Result<Option<Translations<'a>>, Error> {
// load from flash
let flash_data = unsafe { translations::get_blob() };
// check if flash is empty
// TODO perhaps we should check the full area?
if flash_data[0..16] == [super::blob::EMPTY_BYTE; 16] {
return Ok(None);
}
// try to parse the data
Translations::new(flash_data).map(Some)
}
pub fn init() {
// unsafe block because every individual operation here is unsafe
unsafe {
// SAFETY: it is OK to look
if TRANSLATIONS_ON_FLASH.is_some() {
return;
}
// SAFETY: try_init unconditionally loads the translations from flash.
// No other reference exists (TRANSLATIONS_ON_FLASH is None) so this is safe.
match try_init() {
// SAFETY: We are in a single-threaded environment so setting is OK.
// (note that from this point on a reference to flash data is held)
Ok(Some(t)) => TRANSLATIONS_ON_FLASH = Some(t),
Ok(None) => {}
// SAFETY: No reference to flash data exists so it is OK to erase it.
Err(_) => translations::erase(),
}
}
}
// SAFETY: Invalidates all references coming from the flash-based blob.
// In other words, none should exist when this function is called.
pub unsafe fn deinit() {
// SAFETY: Given the above, we can safely clear the cached object.
unsafe { TRANSLATIONS_ON_FLASH = None };
}
// SAFETY: Gives out a reference to a TranslationsBlob which can be invalidated
// by calling `erase()`. The caller must not store this reference, nor any that
// come from it, beyond the lifetime of the current function.
pub unsafe fn get<'a>() -> Option<&'a Translations<'a>> {
// SAFETY: We are in a single-threaded environment.
unsafe { TRANSLATIONS_ON_FLASH.as_ref() }
}

@ -0,0 +1,50 @@
//! generated from ${THIS_FILE.name}
//! (by running `make templates` in `core`)
//! do not edit manually!
#![cfg_attr(rustfmt, rustfmt_skip)]
<%
import json
TR_DIR = ROOT / "core" / "translations"
order_file = TR_DIR / "order.json"
order_index_name = json.loads(order_file.read_text())
order = {int(k): v for k, v in order_index_name.items()}
en_file = TR_DIR / "en.json"
en_data = json.loads(en_file.read_text())["translations"]
%>\
#[cfg(feature = "micropython")]
use crate::micropython::qstr::Qstr;
#[derive(Debug, Copy, Clone, FromPrimitive)]
#[repr(u16)]
#[allow(non_camel_case_types)]
pub enum TranslatedString {
% for idx, name in order.items():
${name} = ${idx},
% endfor
}
impl TranslatedString {
pub fn untranslated(self) -> &'static str {
match self {
% for name in order.values():
Self::${name} => ${json.dumps(en_data.get(name, '""'))},
% endfor
}
}
#[cfg(feature = "micropython")]
pub fn from_qstr(qstr: Qstr) -> Option<Self> {
match qstr {
% for name in order.values():
Qstr::MP_QSTR_${name} => Some(Self::${name}),
% endfor
_ => None,
}
}
}

@ -0,0 +1,33 @@
mod blob;
mod flash;
mod generated;
#[cfg(feature = "micropython")]
mod obj;
mod public_keys;
mod translated_string;
pub use blob::MAX_HEADER_LEN;
pub use translated_string::TranslatedString as TR;
pub const DEFAULT_LANGUAGE: &str = "en-US";
/// # Safety
///
/// Returned pointer will only point to valid font data for as long as
/// the flash content is not invalidated by `erase()` or `write()`.
#[no_mangle]
pub unsafe extern "C" fn get_utf8_glyph(codepoint: cty::uint16_t, font: cty::c_int) -> *const u8 {
// C will send a negative number
let font_abs = font.unsigned_abs() as u16;
// SAFETY: Reference is discarded at the end of the function.
// We do return a _pointer_ to the same memory location, but the pointer is
// always valid.
let Some(tr) = (unsafe { flash::get() }) else {
return core::ptr::null();
};
if let Some(glyph) = tr.font(font_abs).and_then(|t| t.get(codepoint)) {
glyph.as_ptr()
} else {
core::ptr::null()
}
}

@ -0,0 +1,241 @@
use crate::{
error::Error,
io::InputStream,
micropython::{
buffer::{get_buffer, StrBuffer},
ffi,
map::Map,
module::Module,
obj::Obj,
qstr::Qstr,
simple_type::SimpleTypeObj,
typ::Type,
util,
},
trezorhal::translations,
};
use super::translated_string::TranslatedString;
impl TryFrom<TranslatedString> for StrBuffer {
type Error = Error;
fn try_from(value: TranslatedString) -> Result<Self, Self::Error> {
// SAFETY: The translated string is copied into a new memory. Reference to flash
// data is discarded at the end of this function.
let translated = value.translate(unsafe { super::flash::get() });
StrBuffer::alloc(translated)
// TODO fall back to English (which is static and can be converted
// infallibly) if the allocation fails?
}
}
fn translate(translation: TranslatedString) -> Result<Obj, Error> {
// SAFETY: TryFrom<&str> for Obj allocates a copy of the passed in string.
// The reference to flash data is discarded at the end of this function.
let stored_translations = unsafe { super::flash::get() };
translation.translate(stored_translations).try_into()
}
// SAFETY: Caller is supposed to be MicroPython, or copy MicroPython contracts
// about the meaning of arguments.
unsafe extern "C" fn tr_attr_fn(_self_in: Obj, attr: ffi::qstr, dest: *mut Obj) {
let block = || {
let arg = unsafe { dest.read() };
if !arg.is_null() {
// Null destination would mean a `setattr`.
return Err(Error::TypeError);
}
let attr = Qstr::from_u16(attr as u16);
let result = if let Some(translation) = TranslatedString::from_qstr(attr) {
translate(translation)?
} else {
return Err(Error::AttributeError(attr));
};
unsafe { dest.write(result) };
Ok(())
};
unsafe { util::try_or_raise(block) }
}
static TR_TYPE: Type = obj_type! {
name: Qstr::MP_QSTR_TR,
attr_fn: tr_attr_fn,
};
static TR_OBJ: SimpleTypeObj = SimpleTypeObj::new(&TR_TYPE);
fn make_translations_header(header: &super::blob::TranslationsHeader<'_>) -> Result<Obj, Error> {
let version_objs: [Obj; 4] = {
let v = header.version;
[v[0].into(), v[1].into(), v[2].into(), v[3].into()]
};
attr_tuple! {
Qstr::MP_QSTR_language => header.language.try_into()?,
Qstr::MP_QSTR_version => util::new_tuple(&version_objs)?,
Qstr::MP_QSTR_data_len => header.data_len.try_into()?,
Qstr::MP_QSTR_data_hash => header.data_hash.as_ref().try_into()?,
Qstr::MP_QSTR_total_len => header.total_len.try_into()?,
}
}
pub unsafe extern "C" fn translations_header_new(
_self_in: Obj,
n_args: usize,
n_kw: usize,
args: *const Obj,
) -> Obj {
let block = |args: &[Obj], kwargs: &Map| {
if args.len() != 1 || !kwargs.is_empty() {
return Err(Error::TypeError);
}
// SAFETY: reference is discarded at the end of this function.
let buffer = unsafe { get_buffer(args[0])? };
let (header, _) =
super::blob::TranslationsHeader::parse_from(&mut InputStream::new(buffer))?;
make_translations_header(&header)
};
unsafe { util::try_with_args_and_kwargs_inline(n_args, n_kw, args, block) }
}
pub extern "C" fn translations_header_from_flash(_cls_in: Obj) -> Obj {
let block = || {
// SAFETY: reference is discarded at the end of this function.
match unsafe { super::flash::get() } {
Some(translations) => make_translations_header(&translations.header),
None => Ok(Obj::const_none()),
}
};
unsafe { util::try_or_raise(block) }
}
static TRANSLATIONS_HEADER_TYPE: Type = obj_type! {
name: Qstr::MP_QSTR_TranslationsHeader,
locals: &obj_dict!(obj_map! {
Qstr::MP_QSTR_load_from_flash => obj_fn_1!(translations_header_from_flash).as_obj(),
}),
call_fn: translations_header_new,
};
static TRANSLATIONS_HEADER_OBJ: SimpleTypeObj = SimpleTypeObj::new(&TRANSLATIONS_HEADER_TYPE);
extern "C" fn area_bytesize() -> Obj {
let bytesize = translations::area_bytesize();
unsafe { util::try_or_raise(|| bytesize.try_into()) }
}
extern "C" fn get_language() -> Obj {
let block = || {
// SAFETY: reference is discarded at the end of the block
let lang_name = unsafe { super::flash::get() }.map(|t| t.header.language);
lang_name.unwrap_or(super::DEFAULT_LANGUAGE).try_into()
};
unsafe { util::try_or_raise(block) }
}
extern "C" fn init() -> Obj {
let block = || {
super::flash::init();
Ok(Obj::const_none())
};
unsafe { util::try_or_raise(block) }
}
extern "C" fn deinit() -> Obj {
// SAFETY: Safe by itself. Any unsafety stems from some other piece of code
// not upholding the safety parameters.
unsafe { super::flash::deinit() };
Obj::const_none()
}
extern "C" fn erase() -> Obj {
let block = || {
super::flash::erase()?;
Ok(Obj::const_none())
};
unsafe { util::try_or_raise(block) }
}
extern "C" fn write(data: Obj, offset: Obj) -> Obj {
let block = || {
// SAFETY: reference is discarded at the end of the block
let data = unsafe { get_buffer(data)? };
let offset: usize = offset.try_into()?;
super::flash::write(data, offset)?;
Ok(Obj::const_none())
};
unsafe { util::try_or_raise(block) }
}
extern "C" fn verify(data: Obj) -> Obj {
let block = || {
// SAFETY: reference is discarded at the end of the block
let data = unsafe { get_buffer(data)? };
super::blob::Translations::new(data)?;
Ok(Obj::const_none())
};
unsafe { util::try_or_raise(block) }
}
#[no_mangle]
#[rustfmt::skip]
pub static mp_module_trezortranslate: Module = obj_module! {
/// from trezortranslate_keys import TR as TR # noqa: F401
/// """Translation object with attributes."""
Qstr::MP_QSTR_TR => TR_OBJ.as_obj(),
/// def area_bytesize() -> int:
/// """Maximum size of the translation blob that can be stored."""
Qstr::MP_QSTR_area_bytesize => obj_fn_0!(area_bytesize).as_obj(),
/// def get_language() -> str:
/// """Get the current language."""
Qstr::MP_QSTR_get_language => obj_fn_0!(get_language).as_obj(),
/// def init() -> None:
/// """Initialize the translations system.
///
/// Loads and verifies translation data from flash. If the verification passes,
/// Trezor UI is translated from that point forward.
/// """
Qstr::MP_QSTR_init => obj_fn_0!(init).as_obj(),
/// def deinit() -> None:
/// """Deinitialize the translations system.
///
/// Translations must be deinitialized before erasing or writing to flash.
/// """
Qstr::MP_QSTR_deinit => obj_fn_0!(deinit).as_obj(),
/// def erase() -> None:
/// """Erase the translations blob from flash."""
Qstr::MP_QSTR_erase => obj_fn_0!(erase).as_obj(),
/// def write(data: bytes, offset: int) -> None:
/// """Write data to the translations blob in flash."""
Qstr::MP_QSTR_write => obj_fn_2!(write).as_obj(),
/// def verify(data: bytes) -> None:
/// """Verify the translations blob."""
Qstr::MP_QSTR_verify => obj_fn_1!(verify).as_obj(),
/// class TranslationsHeader:
/// """Metadata about the translations blob."""
///
/// language: str
/// version: tuple[int, int, int, int]
/// data_len: int
/// data_hash: bytes
/// total_len: int
///
/// def __init__(self, header_bytes: bytes) -> None:
/// """Parse header from bytes.
/// The header has variable length.
/// """
///
/// @staticmethod
/// def load_from_flash() -> TranslationsHeader | None:
/// """Load translations from flash."""
Qstr::MP_QSTR_TranslationsHeader => TRANSLATIONS_HEADER_OBJ.as_obj(),
};

@ -0,0 +1,15 @@
use crate::crypto::ed25519;
#[cfg(feature = "debug")]
pub const PUBLIC_KEYS_DEVEL: [ed25519::PublicKey; 3] = [
*b"\x68\x46\x0e\xbe\xf3\xb1\x38\x16\x4e\xc7\xfd\x86\x10\xe9\x58\x00\xdf\x75\x98\xf7\x0f\x2f\x2e\xa7\xdb\x51\x72\xac\x74\xeb\xc1\x44",
*b"\x8d\x4a\xbe\x07\x4f\xef\x92\x29\xd3\xb4\x41\xdf\xea\x4f\x98\xf8\x05\xb1\xa2\xb3\xa0\x6a\xe6\x45\x81\x0e\xfe\xce\x77\xfd\x50\x44",
*b"\x97\xf7\x13\x5a\x9a\x26\x90\xe7\x3b\xeb\x26\x55\x6f\x1c\xb1\x63\xbe\xa2\x53\x2a\xff\xa1\xe7\x78\x24\x30\xbe\x98\xc0\xe5\x68\x12",
];
pub const PUBLIC_KEYS: [ed25519::PublicKey; 3] = [
// TODO replace with production keys
*b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f",
*b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f",
*b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f",
];

@ -0,0 +1,41 @@
use crate::strutil::TString;
use super::blob::Translations;
pub use super::generated::translated_string::TranslatedString;
impl TranslatedString {
pub(super) fn translate<'a>(self, source: Option<&'a Translations>) -> &'a str {
source
.and_then(|s| s.translation(self as _))
.unwrap_or(self.untranslated())
}
pub fn map_translated<F, T>(self, fun: F) -> T
where
F: for<'a> FnOnce(&'a str) -> T,
T: 'static,
{
// SAFETY: The bound on F _somehow_ ensures that the reference cannot escape
// the closure. (I don't understand how, but it does), see soundness test below.
// For good measure, we limit the return value to 'static.
let translations = unsafe { super::flash::get() };
fun(self.translate(translations))
}
pub const fn as_tstring(self) -> TString<'static> {
TString::Translation(self)
}
}
// #[cfg(test)]
// mod tests {
// use super::TranslatedString;
// #[test]
// fn test_soundness() {
// let tr = TranslatedString::address__public_key;
// let mut opt: Option<&str> = None;
// tr.map_translated(|s| opt = Some(s));
// assert!(matches!(opt, Some("Address / Public key")));
// }
// }

@ -63,7 +63,7 @@ pub fn char_width(ch: char, font: i32) -> i16 {
text_width(encoding, font)
}
pub fn get_char_glyph(ch: u8, font: i32) -> *const u8 {
pub fn get_char_glyph(ch: u16, font: i32) -> *const u8 {
unsafe { ffi::font_get_glyph(font, ch) }
}

@ -14,6 +14,8 @@ pub mod random;
pub mod rgb_led;
pub mod slip39;
pub mod storage;
#[cfg(feature = "translations")]
pub mod translations;
pub mod usb;
pub mod uzlib;
pub mod wordlist;

@ -0,0 +1,31 @@
use super::ffi;
// SAFETY: Returned slice is valid and immutable until a call to `erase()`
// and/or `set_blob()`. Caller is responsible for disposing of all references to
// the slice before touching the flash contents.
pub unsafe fn get_blob<'a>() -> &'a [u8] {
let mut len: u32 = 0;
let ptr = unsafe { ffi::translations_read(&mut len, 0) };
if ptr.is_null() {
fatal_error!("Translations read failed", "");
}
// SAFETY: The pointer is always valid.
unsafe { core::slice::from_raw_parts(ptr, len as usize) }
}
// SAFETY: This call invalidates the reference to the blob returned by
// `get_blob()`.
pub unsafe fn erase() {
unsafe { ffi::translations_erase() };
}
pub fn area_bytesize() -> usize {
// SAFETY: Safe, no side effects.
unsafe { ffi::translations_area_bytesize() as usize }
}
// SAFETY: This call may invalidate the reference to the blob returned by
// `get_blob()`.
pub unsafe fn write(data: &[u8], offset: usize) -> bool {
unsafe { ffi::translations_write(data.as_ptr(), offset as u32, data.len() as u32) }
}

@ -133,6 +133,6 @@ where
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("Label");
t.string("text", self.text.as_ref());
t.string("text", self.text.as_ref().into());
}
}

@ -234,6 +234,6 @@ where
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("Marquee");
t.string("text", self.text.as_ref());
t.string("text", self.text.as_ref().into());
}
}

@ -151,6 +151,6 @@ impl Component for Qr {
impl crate::trace::Trace for Qr {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("Qr");
t.string("text", self.text.as_ref());
t.string("text", self.text.as_str().into());
}
}

@ -110,6 +110,6 @@ impl<const L: usize> TextBox<L> {
impl<const L: usize> crate::trace::Trace for TextBox<L> {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("TextBox");
t.string("text", &self.text);
t.string("text", self.text.as_str().into());
}
}

@ -541,23 +541,23 @@ pub mod trace {
impl LayoutSink for TraceSink<'_> {
fn text(&mut self, _cursor: Point, _layout: &TextLayout, text: &str) {
self.0.string(text);
self.0.string(&text.into());
}
fn hyphen(&mut self, _cursor: Point, _layout: &TextLayout) {
self.0.string("-");
self.0.string(&"-".into());
}
fn ellipsis(&mut self, _cursor: Point, _layout: &TextLayout) {
self.0.string(ELLIPSIS);
self.0.string(&ELLIPSIS.into());
}
fn prev_page_ellipsis(&mut self, _cursor: Point, _layout: &TextLayout) {
self.0.string(ELLIPSIS);
self.0.string(&ELLIPSIS.into());
}
fn line_break(&mut self, _cursor: Point) {
self.0.string("\n");
self.0.string(&"\n".into());
}
}
}

@ -225,7 +225,7 @@ pub mod trace {
impl<T: ParagraphSource> crate::trace::Trace for Paragraphs<T> {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.string("component", "Paragraphs");
t.string("component", "Paragraphs".into());
t.in_list("paragraphs", &|par_list| {
Self::foreach_visible(
&self.source,

@ -1,6 +1,9 @@
use crate::ui::{
display::{Color, Font},
geometry::{Alignment, Rect},
use crate::{
strutil::TString,
ui::{
display::{Color, Font},
geometry::{Alignment, Rect},
},
};
use super::{
@ -17,7 +20,7 @@ use super::{
/// If it does not fit, returns `None`.
pub fn text_multiline(
area: Rect,
text: &str,
text: TString<'_>,
font: Font,
fg_color: Color,
bg_color: Color,
@ -27,7 +30,7 @@ pub fn text_multiline(
let text_layout = TextLayout::new(text_style)
.with_bounds(area)
.with_align(alignment);
let layout_fit = text_layout.render_text(text);
let layout_fit = text.map(|t| text_layout.render_text(t));
match layout_fit {
LayoutFit::Fitting { height, .. } => Some(area.split_top(height).1),
LayoutFit::OutOfBounds { .. } => None,
@ -38,7 +41,7 @@ pub fn text_multiline(
/// area.
pub fn text_multiline_bottom(
area: Rect,
text: &str,
text: TString<'_>,
font: Font,
fg_color: Color,
bg_color: Color,
@ -50,16 +53,16 @@ pub fn text_multiline_bottom(
.with_align(alignment);
// When text fits the area, displaying it in the bottom part.
// When not, render it "normally".
match text_layout.fit_text(text) {
text.map(|t| match text_layout.fit_text(t) {
LayoutFit::Fitting { height, .. } => {
let (top, bottom) = area.split_bottom(height);
text_layout = text_layout.with_bounds(bottom);
text_layout.render_text(text);
text_layout.render_text(t);
Some(top)
}
LayoutFit::OutOfBounds { .. } => {
text_layout.render_text(text);
text_layout.render_text(t);
None
}
}
})
}

@ -144,6 +144,16 @@ impl Font {
display::text_width(text, self.into())
}
/// Supports UTF8 characters
fn get_first_glyph_from_text(self, text: &str) -> Option<Glyph> {
text.chars().next().map(|c| self.get_glyph(c))
}
/// Supports UTF8 characters
fn get_last_glyph_from_text(self, text: &str) -> Option<Glyph> {
text.chars().next_back().map(|c| self.get_glyph(c))
}
/// 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 {
@ -152,14 +162,20 @@ impl Font {
return 0;
}
let first_char = unwrap!(text.chars().next());
let first_char_glyph = unwrap!(self.get_glyph(first_char as u8));
let first_char_bearing = if let Some(glyph) = self.get_first_glyph_from_text(text) {
glyph.bearing_x
} else {
0
};
let last_char = unwrap!(text.chars().last());
let last_char_glyph = unwrap!(self.get_glyph(last_char as u8));
let last_char_bearing = if let Some(glyph) = self.get_last_glyph_from_text(text) {
glyph.right_side_bearing()
} else {
0
};
// Strip leftmost and rightmost spaces/bearings/margins.
self.text_width(text) - first_char_glyph.bearing_x - last_char_glyph.right_side_bearing()
self.text_width(text) - first_char_bearing - last_char_bearing
}
/// Returning the x-bearing (offset) of the first character.
@ -169,9 +185,11 @@ impl Font {
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
if let Some(glyph) = self.get_first_glyph_from_text(text) {
glyph.bearing_x
} else {
0
}
}
pub fn char_width(self, ch: char) -> i16 {
@ -198,25 +216,21 @@ impl Font {
constant::LINE_SPACE + self.text_height()
}
pub fn get_glyph(self, char_byte: u8) -> Option<Glyph> {
let gl_data = display::get_char_glyph(char_byte, self.into());
pub fn get_glyph(self, ch: char) -> Glyph {
let gl_data = display::get_char_glyph(ch as u16, self.into());
if gl_data.is_null() {
return None;
}
ensure!(!gl_data.is_null(), "Failed to load glyph");
// SAFETY: Glyph::load is valid for data returned by get_char_glyph
unsafe { Some(Glyph::load(gl_data)) }
unsafe { Glyph::load(gl_data) }
}
pub fn display_text(self, text: &str, baseline: Point, fg_color: Color, bg_color: Color) {
let colortable = get_color_table(fg_color, bg_color);
let mut adv_total = 0;
for c in text.bytes() {
let g = self.get_glyph(c);
if let Some(gly) = g {
let adv = gly.print(baseline + Offset::new(adv_total, 0), colortable);
adv_total += adv;
}
for c in text.chars() {
let gly = self.get_glyph(c);
let adv = gly.print(baseline + Offset::new(adv_total, 0), colortable);
adv_total += adv;
}
}

@ -27,6 +27,7 @@ use crate::ui::geometry::Alignment2D;
use crate::{time::Duration, trezorhal::time};
use crate::{
strutil::TString,
trezorhal::{buffers, display, uzlib::UzlibContext},
ui::lerp::Lerp,
};
@ -346,39 +347,39 @@ pub fn clear() {
display::clear();
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub struct TextOverlay<T> {
#[derive(Clone)]
pub struct TextOverlay {
area: Rect,
text: T,
text: TString<'static>,
font: Font,
max_height: i16,
baseline: i16,
}
impl<T: AsRef<str>> TextOverlay<T> {
pub fn new(text: T, font: Font) -> Self {
impl TextOverlay {
pub fn new<T: Into<TString<'static>>>(text: T, font: Font) -> Self {
let area = Rect::zero();
Self {
area,
text,
text: text.into(),
font,
max_height: font.max_height(),
baseline: font.text_baseline(),
}
}
pub fn set_text(&mut self, text: T) {
self.text = text;
pub fn set_text<T: Into<TString<'static>>>(&mut self, text: T) {
self.text = text.into();
}
pub fn get_text(&self) -> &T {
&self.text
pub fn get_text(&self) -> TString<'static> {
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.as_ref());
let text_width = self.text.map(|t| self.font.text_width(t));
let text_height = self.font.text_height();
let text_area_start = baseline + Offset::new(-(text_width / 2), -text_height);
@ -397,30 +398,28 @@ impl<T: AsRef<str>> TextOverlay<T> {
let p_rel = Point::new(p.x - self.area.x0, p.y - self.area.y0);
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),
Point::new(tot_adv + g.bearing_x + g.width, top + g.height),
);
let color = self.text.map(|t| {
for g in t.chars().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),
Point::new(tot_adv + g.bearing_x + g.width, top + g.height),
);
tot_adv += g.adv;
tot_adv += g.adv;
if !char_area.contains(p_rel) {
continue;
}
if !char_area.contains(p_rel) {
continue;
}
let p_inner = p_rel - char_area.top_left();
let overlay_data = g.get_pixel_data(p_inner);
return Color::lerp(underlying, fg, overlay_data as f32 / 15_f32);
}
let p_inner = p_rel - char_area.top_left();
let overlay_data = g.get_pixel_data(p_inner);
return Some(Color::lerp(underlying, fg, overlay_data as f32 / 15_f32));
}
None
});
underlying
color.unwrap_or(underlying)
}
}
@ -995,9 +994,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<T: AsRef<str>>(
pub fn bar_with_text_and_fill(
area: Rect,
overlay: Option<&TextOverlay<T>>,
overlay: Option<&TextOverlay>,
fg_color: Color,
bg_color: Color,
fill_from: i16,

@ -32,7 +32,7 @@ impl ReturnToC for IntroMsg {
pub struct Intro<'a> {
bg: Pad,
title: Child<Label<&'a str>>,
buttons: Child<ButtonController<&'static str>>,
buttons: Child<ButtonController>,
text: Child<Label<&'a str>>,
warn: Option<Child<Label<&'a str>>>,
}
@ -43,8 +43,8 @@ impl<'a> Intro<'a> {
bg: Pad::with_background(BLD_BG).with_clear(),
title: Child::new(Label::centered(title, TEXT_NORMAL).vertically_centered()),
buttons: Child::new(ButtonController::new(ButtonLayout::text_none_text(
LEFT_BUTTON_TEXT,
RIGHT_BUTTON_TEXT,
LEFT_BUTTON_TEXT.into(),
RIGHT_BUTTON_TEXT.into(),
))),
text: Child::new(Label::left_aligned(content, TEXT_NORMAL).vertically_centered()),
warn: (!fw_ok).then_some(Child::new(

@ -13,7 +13,7 @@ use crate::{
use super::{
super::{
component::{Choice, ChoiceFactory, ChoicePage},
component::{ButtonLayout, Choice, ChoiceFactory, ChoicePage},
theme::bootloader::{BLD_BG, BLD_FG, ICON_EXIT, ICON_REDO, ICON_TRASH},
},
ReturnToC,
@ -51,7 +51,7 @@ impl MenuChoice {
}
}
impl Choice<&'static str> for MenuChoice {
impl Choice for MenuChoice {
fn paint_center(&self, _area: Rect, _inverse: bool) {
// Icon on top and two lines of text below
self.icon.draw(
@ -70,6 +70,10 @@ impl Choice<&'static str> for MenuChoice {
BLD_BG,
);
}
fn btn_layout(&self) -> ButtonLayout {
ButtonLayout::arrow_armed_arrow("SELECT".into())
}
}
#[cfg(feature = "ui_debug")]
@ -95,7 +99,7 @@ impl MenuChoiceFactory {
}
}
impl ChoiceFactory<&'static str> for MenuChoiceFactory {
impl ChoiceFactory for MenuChoiceFactory {
type Action = MenuMsg;
type Item = MenuChoice;
@ -125,7 +129,7 @@ impl ChoiceFactory<&'static str> for MenuChoiceFactory {
pub struct Menu {
pad: Pad,
choice_page: Child<ChoicePage<MenuChoiceFactory, &'static str, MenuMsg>>,
choice_page: Child<ChoicePage<MenuChoiceFactory, MenuMsg>>,
}
impl Menu {

@ -2,7 +2,8 @@ use heapless::Vec;
use crate::{
error::Error,
strutil::StringType,
micropython::buffer::StrBuffer,
translations::TR,
ui::{
component::{
text::paragraphs::{Paragraph, ParagraphSource, ParagraphVecShort, Paragraphs, VecExt},
@ -19,40 +20,40 @@ use super::{
const MAX_XPUBS: usize = 16;
const QR_BORDER: i16 = 3;
pub struct AddressDetails<T>
where
T: StringType,
{
pub struct AddressDetails {
qr_code: Qr,
details_view: Paragraphs<ParagraphVecShort<T>>,
xpub_view: Frame<Paragraphs<Paragraph<T>>, T>,
xpubs: Vec<(T, T), MAX_XPUBS>,
details_view: Paragraphs<ParagraphVecShort<StrBuffer>>,
xpub_view: Frame<Paragraphs<Paragraph<StrBuffer>>, StrBuffer>,
xpubs: Vec<(StrBuffer, StrBuffer), MAX_XPUBS>,
current_page: usize,
current_subpage: usize,
area: Rect,
pad: Pad,
buttons: Child<ButtonController<T>>,
buttons: Child<ButtonController>,
}
impl<T> AddressDetails<T>
where
T: StringType + Clone,
{
impl AddressDetails {
pub fn new(
qr_address: T,
qr_address: StrBuffer,
case_sensitive: bool,
account: Option<T>,
path: Option<T>,
account: Option<StrBuffer>,
path: Option<StrBuffer>,
) -> Result<Self, Error> {
let qr_code = Qr::new(qr_address, case_sensitive)?.with_border(QR_BORDER);
let details_view = {
let mut para = ParagraphVecShort::new();
if let Some(account) = account {
para.add(Paragraph::new(&theme::TEXT_BOLD, "Account:".into()));
para.add(Paragraph::new(
&theme::TEXT_BOLD,
TR::words__account_colon.try_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_BOLD,
TR::address_details__derivation_path.try_into()?,
));
para.add(Paragraph::new(&theme::TEXT_MONO, path));
}
Paragraphs::new(para)
@ -76,7 +77,7 @@ where
Ok(result)
}
pub fn add_xpub(&mut self, title: T, xpub: T) -> Result<(), Error> {
pub fn add_xpub(&mut self, title: StrBuffer, xpub: StrBuffer) -> Result<(), Error> {
self.xpubs
.push((title, xpub))
.map_err(|_| Error::OutOfRange)
@ -111,7 +112,7 @@ where
/// last page. On xpub pages there is SHOW ALL 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> {
fn get_button_layout(&mut self) -> ButtonLayout {
let (left, middle, right) = if self.is_in_subpage() {
let left = Some(ButtonDetails::up_arrow_icon_wide());
let right = if self.is_last_subpage() {
@ -123,7 +124,7 @@ where
} 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("SHOW ALL".into()))
Some(ButtonDetails::armed_text(TR::buttons__show_all.into()))
} else {
None
};
@ -151,9 +152,9 @@ where
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_title(ctx, self.xpubs[i].0);
self.xpub_view.update_content(ctx, |p| {
p.inner_mut().update(self.xpubs[i].1.clone());
p.inner_mut().update(self.xpubs[i].1);
p.change_page(0)
});
}
@ -175,10 +176,7 @@ where
}
}
impl<T> Component for AddressDetails<T>
where
T: StringType + Clone,
{
impl Component for AddressDetails {
type Msg = ();
fn place(&mut self, bounds: Rect) -> Rect {
@ -265,10 +263,7 @@ where
}
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for AddressDetails<T>
where
T: StringType + Clone,
{
impl crate::trace::Trace for AddressDetails {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("AddressDetails");
match self.current_page {

@ -1,5 +1,5 @@
use crate::{
strutil::StringType,
strutil::TString,
ui::{
component::{Child, Component, ComponentExt, Event, EventCtx, Label, Pad},
display::{self, Color, Font},
@ -20,40 +20,40 @@ pub enum ConfirmMsg {
Confirm = 2,
}
pub struct Confirm<T: StringType, U> {
pub struct Confirm<U> {
bg: Pad,
bg_color: Color,
title: &'static str,
title: TString<'static>,
message: Child<Label<U>>,
alert: Option<Label<T>>,
info_title: Option<T>,
alert: Option<Label<U>>,
info_title: Option<TString<'static>>,
info_text: Option<Label<U>>,
button_text: T,
buttons: ButtonController<T>,
button_text: TString<'static>,
buttons: ButtonController,
/// Whether we are on the info screen (optional extra screen)
showing_info_screen: bool,
two_btn_confirm: bool,
}
impl<T, U> Confirm<T, U>
impl<U> Confirm<U>
where
T: StringType + Clone,
U: AsRef<str>,
{
pub fn new(
pub fn new<T: Into<TString<'static>>>(
bg_color: Color,
title: &'static str,
title: T,
message: Label<U>,
alert: Option<Label<T>>,
alert: Option<Label<U>>,
button_text: T,
two_btn_confirm: bool,
) -> Self {
let button_text = button_text.into();
let btn_layout =
Self::get_button_layout_general(false, button_text.clone(), false, two_btn_confirm);
Self::get_button_layout_general(false, button_text, false, two_btn_confirm);
Self {
bg: Pad::with_background(bg_color).with_clear(),
bg_color,
title,
title: title.into(),
message: Child::new(message),
alert,
info_title: None,
@ -66,8 +66,12 @@ where
}
/// Adding optional info screen
pub fn with_info_screen(mut self, info_title: T, info_text: Label<U>) -> Self {
self.info_title = Some(info_title);
pub fn with_info_screen<T: Into<TString<'static>>>(
mut self,
info_title: T,
info_text: Label<U>,
) -> Self {
self.info_title = Some(info_title.into());
self.info_text = Some(info_text);
self.buttons = ButtonController::new(self.get_button_layout());
self
@ -77,10 +81,10 @@ where
self.info_title.is_some()
}
fn get_button_layout(&self) -> ButtonLayout<T> {
fn get_button_layout(&self) -> ButtonLayout {
Self::get_button_layout_general(
self.showing_info_screen,
self.button_text.clone(),
self.button_text,
self.has_info_screen(),
self.two_btn_confirm,
)
@ -89,10 +93,10 @@ where
/// Not relying on self here, to call it in constructor.
fn get_button_layout_general(
showing_info_screen: bool,
button_text: T,
button_text: TString<'static>,
has_info_screen: bool,
two_btn_confirm: bool,
) -> ButtonLayout<T> {
) -> ButtonLayout {
if showing_info_screen {
ButtonLayout::arrow_none_none()
} else if has_info_screen {
@ -121,9 +125,8 @@ where
}
}
impl<T, U> Component for Confirm<T, U>
impl<U> Component for Confirm<U>
where
T: StringType + Clone,
U: AsRef<str>,
{
type Msg = ConfirmMsg;
@ -204,13 +207,17 @@ where
fn paint(&mut self) {
self.bg.paint();
let display_top_left = |text: &str| {
display::text_top_left(Point::zero(), text, Font::BOLD, WHITE, self.bg_color);
let display_top_left = |text: TString<'static>| {
text.map(|t| {
display::text_top_left(Point::zero(), t, Font::BOLD, WHITE, self.bg_color)
});
};
// We are either on the info screen or on the "main" screen
if self.showing_info_screen {
display_top_left(unwrap!(self.info_title.clone()).as_ref());
if let Some(title) = self.info_title {
display_top_left(title);
}
self.info_text.paint();
} else {
display_top_left(self.title);
@ -227,9 +234,8 @@ where
}
#[cfg(feature = "ui_debug")]
impl<T, U> crate::trace::Trace for Confirm<T, U>
impl<U> crate::trace::Trace for Confirm<U>
where
T: StringType + Clone,
U: AsRef<str>,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {

@ -1,5 +1,5 @@
use crate::{
strutil::StringType,
strutil::TString,
time::Duration,
ui::{
component::{Component, Event, EventCtx, Never},
@ -30,22 +30,16 @@ impl From<PhysicalButton> for ButtonPos {
}
}
pub struct Button<T>
where
T: StringType,
{
pub struct Button {
bounds: Rect,
pos: ButtonPos,
content: ButtonContent<T>,
content: ButtonContent,
styles: ButtonStyleSheet,
state: State,
}
impl<T> Button<T>
where
T: StringType,
{
pub fn new(pos: ButtonPos, content: ButtonContent<T>, styles: ButtonStyleSheet) -> Self {
impl Button {
pub fn new(pos: ButtonPos, content: ButtonContent, styles: ButtonStyleSheet) -> Self {
Self {
pos,
content,
@ -55,7 +49,7 @@ where
}
}
pub fn from_button_details(pos: ButtonPos, btn_details: ButtonDetails<T>) -> Self {
pub fn from_button_details(pos: ButtonPos, btn_details: ButtonDetails) -> Self {
// Deciding between text and icon
let style = btn_details.style();
match btn_details.content {
@ -64,7 +58,7 @@ where
}
}
pub fn with_text(pos: ButtonPos, text: T, styles: ButtonStyleSheet) -> Self {
pub fn with_text(pos: ButtonPos, text: TString<'static>, styles: ButtonStyleSheet) -> Self {
Self::new(pos, ButtonContent::Text(text), styles)
}
@ -72,7 +66,7 @@ where
Self::new(pos, ButtonContent::Icon(image), styles)
}
pub fn content(&self) -> &ButtonContent<T> {
pub fn content(&self) -> &ButtonContent {
&self.content
}
@ -89,7 +83,7 @@ where
}
/// Changing the text content of the button.
pub fn set_text(&mut self, text: T) {
pub fn set_text(&mut self, text: TString<'static>) {
self.content = ButtonContent::Text(text);
}
@ -127,7 +121,7 @@ where
} else {
match &self.content {
ButtonContent::Text(text) => {
let text_width = style.font.visible_text_width(text.as_ref());
let text_width = text.map(|t| style.font.visible_text_width(t));
if style.with_outline {
text_width + 2 * theme::BUTTON_OUTLINE
} else if style.with_arms {
@ -176,7 +170,7 @@ where
// Centering the text in case of fixed width.
if let ButtonContent::Text(text) = &self.content {
if let Some(fixed_width) = style.fixed_width {
let diff = fixed_width - style.font.visible_text_width(text.as_ref());
let diff = fixed_width - text.map(|t| style.font.visible_text_width(t));
offset_x = diff / 2;
}
}
@ -185,10 +179,7 @@ where
}
}
impl<T> Component for Button<T>
where
T: StringType,
{
impl Component for Button {
type Msg = Never;
fn place(&mut self, bounds: Rect) -> Rect {
@ -237,16 +228,15 @@ where
// Painting the content
match &self.content {
ButtonContent::Text(text) => {
ButtonContent::Text(text) => text.map(|t| {
display::text_left(
self.get_text_baseline(style)
- Offset::x(style.font.start_x_bearing(text.as_ref())),
text.as_ref(),
self.get_text_baseline(style) - Offset::x(style.font.start_x_bearing(t)),
t,
style.font,
fg_color,
bg_color,
);
}
}),
ButtonContent::Icon(icon) => {
// Allowing for possible offset of the area from current style
let icon_area = area.translate(style.offset);
@ -284,8 +274,8 @@ enum State {
}
#[derive(Clone)]
pub enum ButtonContent<T> {
Text(T),
pub enum ButtonContent {
Text(TString<'static>),
Icon(Icon),
}
@ -352,8 +342,8 @@ impl ButtonStyleSheet {
/// Describing the button on the screen - only visuals.
#[derive(Clone)]
pub struct ButtonDetails<T> {
pub content: ButtonContent<T>,
pub struct ButtonDetails {
pub content: ButtonContent,
pub duration: Option<Duration>,
with_outline: bool,
with_arms: bool,
@ -362,12 +352,9 @@ pub struct ButtonDetails<T> {
pub send_long_press: bool,
}
impl<T> ButtonDetails<T>
where
T: StringType,
{
impl ButtonDetails {
/// Text button.
pub fn text(text: T) -> Self {
pub fn text(text: TString<'static>) -> Self {
Self {
content: ButtonContent::Text(text),
duration: None,
@ -393,17 +380,17 @@ where
}
/// Resolves text and finds possible icon names.
pub fn from_text_possible_icon(text: T) -> Self {
match text.as_ref() {
pub fn from_text_possible_icon(text: TString<'static>) -> Self {
text.map(|t| match t {
"" => Self::cancel_icon(),
"<" => Self::left_arrow_icon(),
"^" => Self::up_arrow_icon(),
_ => Self::text(text),
}
})
}
/// Text with arms signalling double press.
pub fn armed_text(text: T) -> Self {
pub fn armed_text(text: TString<'static>) -> Self {
Self::text(text).with_arms()
}
@ -495,20 +482,17 @@ where
/// Holding the button details for all three possible buttons.
#[derive(Clone)]
pub struct ButtonLayout<T> {
pub btn_left: Option<ButtonDetails<T>>,
pub btn_middle: Option<ButtonDetails<T>>,
pub btn_right: Option<ButtonDetails<T>>,
pub struct ButtonLayout {
pub btn_left: Option<ButtonDetails>,
pub btn_middle: Option<ButtonDetails>,
pub btn_right: Option<ButtonDetails>,
}
impl<T> ButtonLayout<T>
where
T: StringType,
{
impl ButtonLayout {
pub fn new(
btn_left: Option<ButtonDetails<T>>,
btn_middle: Option<ButtonDetails<T>>,
btn_right: Option<ButtonDetails<T>>,
btn_left: Option<ButtonDetails>,
btn_middle: Option<ButtonDetails>,
btn_right: Option<ButtonDetails>,
) -> Self {
Self {
btn_left,
@ -523,13 +507,8 @@ where
Self::new(None, None, None)
}
/// Default button layout for all three buttons - icons.
pub fn default_three_icons() -> Self {
Self::arrow_armed_arrow("SELECT".into())
}
/// Special middle text for default icon layout.
pub fn arrow_armed_arrow(text: T) -> Self {
/// Arrows at sides, armed text in the middle.
pub fn arrow_armed_arrow(text: TString<'static>) -> Self {
Self::new(
Some(ButtonDetails::left_arrow_icon()),
Some(ButtonDetails::armed_text(text)),
@ -538,7 +517,7 @@ where
}
/// Left cancel, armed text and next right arrow.
pub fn cancel_armed_arrow(text: T) -> Self {
pub fn cancel_armed_arrow(text: TString<'static>) -> Self {
Self::new(
Some(ButtonDetails::cancel_icon()),
Some(ButtonDetails::armed_text(text)),
@ -547,7 +526,7 @@ where
}
/// Middle armed text and next right arrow.
pub fn none_armed_arrow(text: T) -> Self {
pub fn none_armed_arrow(text: TString<'static>) -> Self {
Self::new(
None,
Some(ButtonDetails::armed_text(text)),
@ -556,7 +535,7 @@ where
}
/// Left text, armed text and right info icon/text.
pub fn text_armed_info(left: T, middle: T) -> Self {
pub fn text_armed_info(left: TString<'static>, middle: TString<'static>) -> Self {
Self::new(
Some(ButtonDetails::from_text_possible_icon(left)),
Some(ButtonDetails::armed_text(middle)),
@ -565,7 +544,7 @@ where
}
/// Left cancel, armed text and right info icon/text.
pub fn cancel_armed_info(middle: T) -> Self {
pub fn cancel_armed_info(middle: TString<'static>) -> Self {
Self::new(
Some(ButtonDetails::cancel_icon()),
Some(ButtonDetails::armed_text(middle)),
@ -574,7 +553,7 @@ where
}
/// Left cancel, armed text and blank on right.
pub fn cancel_armed_none(middle: T) -> Self {
pub fn cancel_armed_none(middle: TString<'static>) -> Self {
Self::new(
Some(ButtonDetails::cancel_icon()),
Some(ButtonDetails::armed_text(middle)),
@ -583,7 +562,7 @@ where
}
/// Left back arrow and middle armed text.
pub fn arrow_armed_none(text: T) -> Self {
pub fn arrow_armed_none(text: TString<'static>) -> Self {
Self::new(
Some(ButtonDetails::left_arrow_icon()),
Some(ButtonDetails::armed_text(text)),
@ -592,7 +571,7 @@ where
}
/// Left and right texts.
pub fn text_none_text(left: T, right: T) -> Self {
pub fn text_none_text(left: TString<'static>, right: TString<'static>) -> Self {
Self::new(
Some(ButtonDetails::from_text_possible_icon(left)),
None,
@ -601,7 +580,7 @@ where
}
/// Left text and right arrow.
pub fn text_none_arrow(text: T) -> Self {
pub fn text_none_arrow(text: TString<'static>) -> Self {
Self::new(
Some(ButtonDetails::from_text_possible_icon(text)),
None,
@ -610,7 +589,7 @@ where
}
/// Left text and WIDE right arrow.
pub fn text_none_arrow_wide(text: T) -> Self {
pub fn text_none_arrow_wide(text: TString<'static>) -> Self {
Self::new(
Some(ButtonDetails::from_text_possible_icon(text)),
None,
@ -619,7 +598,7 @@ where
}
/// Only right text.
pub fn none_none_text(text: T) -> Self {
pub fn none_none_text(text: TString<'static>) -> Self {
Self::new(
None,
None,
@ -637,7 +616,7 @@ where
}
/// Left arrow and right text.
pub fn arrow_none_text(text: T) -> Self {
pub fn arrow_none_text(text: TString<'static>) -> Self {
Self::new(
Some(ButtonDetails::left_arrow_icon()),
None,
@ -646,7 +625,7 @@ where
}
/// Up arrow left and right text.
pub fn up_arrow_none_text(text: T) -> Self {
pub fn up_arrow_none_text(text: TString<'static>) -> Self {
Self::new(
Some(ButtonDetails::up_arrow_icon()),
None,
@ -682,7 +661,7 @@ where
}
/// Up arrow on left, middle text and info on the right.
pub fn up_arrow_armed_info(text: T) -> Self {
pub fn up_arrow_armed_info(text: TString<'static>) -> Self {
Self::new(
Some(ButtonDetails::up_arrow_icon()),
Some(ButtonDetails::armed_text(text)),
@ -700,7 +679,7 @@ where
}
/// Cancel cross on left and text on the right.
pub fn cancel_none_text(text: T) -> Self {
pub fn cancel_none_text(text: TString<'static>) -> Self {
Self::new(
Some(ButtonDetails::cancel_icon()),
None,
@ -709,7 +688,7 @@ where
}
/// Cancel cross on left and hold-to-confirm text on the right.
pub fn cancel_none_htc(text: T) -> Self {
pub fn cancel_none_htc(text: TString<'static>) -> Self {
Self::new(
Some(ButtonDetails::cancel_icon()),
None,
@ -718,7 +697,7 @@ where
}
/// Arrow back on left and hold-to-confirm text on the right.
pub fn arrow_none_htc(text: T) -> Self {
pub fn arrow_none_htc(text: TString<'static>) -> Self {
Self::new(
Some(ButtonDetails::left_arrow_icon()),
None,
@ -727,12 +706,12 @@ where
}
/// Only armed text in the middle.
pub fn none_armed_none(text: T) -> Self {
pub fn none_armed_none(text: TString<'static>) -> Self {
Self::new(None, Some(ButtonDetails::armed_text(text)), None)
}
/// HTC on both sides.
pub fn htc_none_htc(left: T, right: T) -> Self {
pub fn htc_none_htc(left: TString<'static>, right: TString<'static>) -> Self {
Self::new(
Some(ButtonDetails::text(left).with_default_duration()),
None,
@ -945,33 +924,30 @@ impl ButtonActions {
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
use crate::strutil::ShortString;
#[cfg(feature = "ui_debug")]
impl<T: StringType> crate::trace::Trace for Button<T> {
impl crate::trace::Trace for Button {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("Button");
match &self.content {
ButtonContent::Text(text) => t.string("text", text.as_ref()),
match self.content {
ButtonContent::Text(text) => t.string("text", text),
ButtonContent::Icon(icon) => {
t.null("text");
t.string("icon", icon.name);
t.string("icon", icon.name.into());
}
}
}
}
#[cfg(feature = "ui_debug")]
impl<T: StringType> crate::trace::Trace for ButtonDetails<T> {
impl crate::trace::Trace for ButtonDetails {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("ButtonDetails");
match &self.content {
match self.content {
ButtonContent::Text(text) => {
t.string("text", text.as_ref());
t.string("text", text);
}
ButtonContent::Icon(icon) => {
t.null("text");
t.string("icon", icon.name);
t.string("icon", icon.name.into());
}
}
if let Some(duration) = &self.duration {
@ -979,19 +955,3 @@ impl<T: StringType> crate::trace::Trace for ButtonDetails<T> {
}
}
}
#[cfg(feature = "ui_debug")]
impl ButtonAction {
/// Describing the action as a string. Debug-only.
pub fn string(&self) -> ShortString {
match self {
ButtonAction::NextPage => "Next".into(),
ButtonAction::PrevPage => "Prev".into(),
ButtonAction::FirstPage => "First".into(),
ButtonAction::LastPage => "Last".into(),
ButtonAction::Cancel => "Cancel".into(),
ButtonAction::Confirm => "Confirm".into(),
ButtonAction::Info => "Info".into(),
}
}
}

@ -2,7 +2,6 @@ use super::{
theme, Button, ButtonDetails, ButtonLayout, ButtonPos, HoldToConfirm, HoldToConfirmMsg,
};
use crate::{
strutil::StringType,
time::{Duration, Instant},
ui::{
component::{base::Event, Component, EventCtx, Pad, TimerToken},
@ -52,20 +51,14 @@ pub enum ButtonControllerMsg {
}
/// Defines what kind of button should be currently used.
pub enum ButtonType<T>
where
T: StringType,
{
Button(Button<T>),
HoldToConfirm(HoldToConfirm<T>),
pub enum ButtonType {
Button(Button),
HoldToConfirm(HoldToConfirm),
Nothing,
}
impl<T> ButtonType<T>
where
T: StringType,
{
pub fn from_button_details(pos: ButtonPos, btn_details: Option<ButtonDetails<T>>) -> Self {
impl ButtonType {
pub fn from_button_details(pos: ButtonPos, btn_details: Option<ButtonDetails>) -> Self {
if let Some(btn_details) = btn_details {
if btn_details.duration.is_some() {
Self::HoldToConfirm(HoldToConfirm::from_button_details(pos, btn_details))
@ -107,12 +100,9 @@ where
///
/// 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,
{
pub struct ButtonContainer {
pos: ButtonPos,
button_type: ButtonType<T>,
button_type: ButtonType,
/// Holds the timestamp of when the button was pressed.
pressed_since: Option<Instant>,
/// How long the button should be pressed to send `long_press=true` in
@ -125,13 +115,10 @@ where
send_long_press: bool,
}
impl<T> ButtonContainer<T>
where
T: StringType,
{
impl ButtonContainer {
/// 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 {
pub fn new(pos: ButtonPos, btn_details: Option<ButtonDetails>) -> Self {
const DEFAULT_LONG_PRESS_MS: u32 = 1000;
let send_long_press = btn_details
.as_ref()
@ -149,7 +136,7 @@ where
/// 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) {
pub fn set(&mut self, btn_details: Option<ButtonDetails>, button_area: Rect) {
self.send_long_press = btn_details
.as_ref()
.map_or(false, |btn| btn.send_long_press);
@ -258,14 +245,11 @@ where
///
/// There is optional complexity with IgnoreButtonDelay, which gets executed
/// only in cases where clients request it.
pub struct ButtonController<T>
where
T: StringType,
{
pub struct ButtonController {
pad: Pad,
left_btn: ButtonContainer<T>,
middle_btn: ButtonContainer<T>,
right_btn: ButtonContainer<T>,
left_btn: ButtonContainer,
middle_btn: ButtonContainer,
right_btn: ButtonContainer,
state: ButtonState,
/// Button area is needed so the buttons
/// can be "re-placed" after their text is changed
@ -277,11 +261,8 @@ where
handle_middle_button: bool,
}
impl<T> ButtonController<T>
where
T: StringType,
{
pub fn new(btn_layout: ButtonLayout<T>) -> Self {
impl ButtonController {
pub fn new(btn_layout: ButtonLayout) -> Self {
let handle_middle_button = btn_layout.btn_middle.is_some();
Self {
pad: Pad::with_background(theme::BG).with_clear(),
@ -303,7 +284,7 @@ where
}
/// Updating all the three buttons to the wanted states.
pub fn set(&mut self, btn_layout: ButtonLayout<T>) {
pub fn set(&mut self, btn_layout: ButtonLayout) {
self.handle_middle_button = btn_layout.btn_middle.is_some();
self.pad.clear();
self.left_btn.set(btn_layout.btn_left, self.button_area);
@ -406,10 +387,7 @@ where
}
}
impl<T> Component for ButtonController<T>
where
T: StringType,
{
impl Component for ButtonController {
type Msg = ButtonControllerMsg;
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
@ -794,7 +772,7 @@ impl Component for AutomaticMover {
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
impl<T: StringType> crate::trace::Trace for ButtonContainer<T> {
impl crate::trace::Trace for ButtonContainer {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
if let ButtonType::Button(btn) = &self.button_type {
btn.trace(t);
@ -805,7 +783,7 @@ impl<T: StringType> crate::trace::Trace for ButtonContainer<T> {
}
#[cfg(feature = "ui_debug")]
impl<T: StringType> crate::trace::Trace for ButtonController<T> {
impl crate::trace::Trace for ButtonController {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("ButtonController");
t.child("left_btn", &self.left_btn);

@ -2,6 +2,7 @@ use core::mem;
use crate::{
strutil::StringType,
translations::TR,
ui::{
component::{
base::Never,
@ -16,8 +17,6 @@ use crate::{
use super::theme;
const HEADER: &str = "COINJOIN IN PROGRESS";
const FOOTER: &str = "Do not disconnect your Trezor!";
const FOOTER_TEXT_MARGIN: i16 = 8;
const LOADER_OFFSET: i16 = -15;
const LOADER_SPEED: u16 = 10;
@ -89,7 +88,7 @@ where
if self.indeterminate {
text_multiline(
self.area,
HEADER,
TR::coinjoin__title_progress.into(),
Font::BOLD,
theme::FG,
theme::BG,
@ -114,7 +113,7 @@ where
// BOTTOM
let top_rest = text_multiline_bottom(
self.area,
FOOTER,
TR::coinjoin__do_not_disconnect.into(),
Font::BOLD,
theme::FG,
theme::BG,
@ -123,7 +122,7 @@ where
if let Some(rest) = top_rest {
text_multiline_bottom(
rest.inset(Insets::bottom(FOOTER_TEXT_MARGIN)),
self.text.as_ref(),
self.text.as_ref().into(),
Font::NORMAL,
theme::FG,
theme::BG,
@ -140,8 +139,8 @@ where
{
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);
t.string("header", TR::coinjoin__title_progress.into());
t.string("text", self.text.as_ref().into());
t.string("footer", TR::coinjoin__do_not_disconnect.into());
}
}

@ -6,23 +6,23 @@ use crate::ui::{
use super::theme;
/// Display white text on black background
pub fn display_left<T: AsRef<str>>(baseline: Point, text: &T, font: Font) {
pub fn display_left<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) {
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) {
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) {
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);
}

@ -26,7 +26,7 @@ where
content_area: Rect,
title_area: Rect,
pad: Pad,
buttons: Child<ButtonController<T>>,
buttons: Child<ButtonController>,
page_counter: usize,
return_confirmed_index: bool,
show_scrollbar: bool,

@ -79,7 +79,7 @@ where
T: StringType + Clone,
{
formatted: FormattedText<T>,
btn_layout: ButtonLayout<T>,
btn_layout: ButtonLayout,
btn_actions: ButtonActions,
current_page: usize,
page_count: usize,
@ -93,7 +93,7 @@ where
T: StringType + Clone,
{
pub fn new(
btn_layout: ButtonLayout<T>,
btn_layout: ButtonLayout,
btn_actions: ButtonActions,
formatted: FormattedText<T>,
) -> Self {
@ -137,7 +137,7 @@ where
bounds
}
pub fn btn_layout(&self) -> ButtonLayout<T> {
pub fn btn_layout(&self) -> ButtonLayout {
// When we are in pagination inside this flow,
// show the up and down arrows on appropriate sides.
let current = self.btn_layout.clone();
@ -235,7 +235,7 @@ where
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.string("page_title", title.as_ref().into());
}
t.int("active_page", self.current_page as i64);
t.int("page_count", self.page_count as i64);

@ -1,5 +1,5 @@
use crate::{
strutil::StringType,
strutil::TString,
time::{Duration, Instant},
ui::{
component::{Component, Event, EventCtx},
@ -18,21 +18,21 @@ pub enum HoldToConfirmMsg {
FailedToConfirm,
}
pub struct HoldToConfirm<T>
where
T: StringType,
{
pub struct HoldToConfirm {
pos: ButtonPos,
loader: Loader<T>,
loader: Loader,
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());
impl HoldToConfirm {
pub fn text<T: Into<TString<'static>>>(
pos: ButtonPos,
text: T,
styles: LoaderStyleSheet,
duration: Duration,
) -> Self {
let text = text.into();
let text_width = text.map(|t| styles.normal.font.visible_text_width(t));
Self {
pos,
loader: Loader::text(text, styles).with_growing_duration(duration),
@ -40,7 +40,7 @@ where
}
}
pub fn from_button_details(pos: ButtonPos, btn_details: ButtonDetails<T>) -> Self {
pub fn from_button_details(pos: ButtonPos, btn_details: ButtonDetails) -> Self {
let duration = btn_details
.duration
.unwrap_or_else(|| Duration::from_millis(DEFAULT_DURATION_MS));
@ -53,7 +53,8 @@ where
}
/// Updating the text of the component and re-placing it.
pub fn set_text(&mut self, text: T, button_area: Rect) {
pub fn set_text<T: Into<TString<'static>>>(&mut self, text: T, button_area: Rect) {
let text = text.into();
self.text_width = self.loader.get_text_width(&text);
self.loader.set_text(text);
self.place(button_area);
@ -71,7 +72,7 @@ where
self.loader.get_duration()
}
pub fn get_text(&self) -> &T {
pub fn get_text(&self) -> TString<'static> {
self.loader.get_text()
}
@ -85,10 +86,7 @@ where
}
}
impl<T> Component for HoldToConfirm<T>
where
T: StringType,
{
impl Component for HoldToConfirm {
type Msg = HoldToConfirmMsg;
fn place(&mut self, bounds: Rect) -> Rect {
@ -129,10 +127,7 @@ where
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for HoldToConfirm<T>
where
T: StringType,
{
impl crate::trace::Trace for HoldToConfirm {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("HoldToConfirm");
t.child("loader", &self.loader);

@ -1,5 +1,8 @@
use crate::{
error::Error,
micropython::buffer::StrBuffer,
strutil::StringType,
translations::TR,
trezorhal::usb::usb_configured,
ui::{
component::{Child, Component, Event, EventCtx, Label},
@ -60,7 +63,7 @@ where
label: Label<T>,
notification: Option<(T, u8)>,
/// Used for HTC functionality to lock device from homescreen
invisible_buttons: Child<ButtonController<T>>,
invisible_buttons: Child<ButtonController>,
/// Holds the loader component
loader: Option<Child<ProgressLoader<T>>>,
/// Whether to show the loader or not
@ -107,10 +110,11 @@ where
if !usb_configured() {
self.fill_notification_background();
// TODO: fill warning icons here as well?
display_center(baseline, &"NO USB CONNECTION", NOTIFICATION_FONT);
TR::homescreen__title_no_usb_connection
.map_translated(|t| display_center(baseline, t, NOTIFICATION_FONT));
} else if let Some((notification, _level)) = &self.notification {
self.fill_notification_background();
display_center(baseline, &notification.as_ref(), NOTIFICATION_FONT);
display_center(baseline, notification.as_ref(), NOTIFICATION_FONT);
// Painting warning icons in top corners when the text is short enough not to
// collide with them
let icon_width = NOTIFICATION_ICON.toif.width();
@ -239,9 +243,9 @@ where
T: StringType,
{
label: Child<Label<T>>,
instruction: Child<Label<T>>,
instruction: Child<Label<StrBuffer>>,
/// Used for unlocking the device from lockscreen
invisible_buttons: Child<ButtonController<T>>,
invisible_buttons: Child<ButtonController>,
/// Display coinjoin icon?
coinjoin_icon: Option<Icon>,
/// Screensaver mode (keep screen black)
@ -252,22 +256,25 @@ impl<T> Lockscreen<T>
where
T: StringType + Clone,
{
pub fn new(label: T, bootscreen: bool, coinjoin_authorized: bool) -> Self {
pub fn new(label: T, bootscreen: bool, coinjoin_authorized: bool) -> Result<Self, Error> {
// Buttons will not be visible, we only need all three of them to be present,
// so that even middle-click triggers the event.
let invisible_btn_layout = ButtonLayout::arrow_armed_arrow("".into());
let instruction_str = if bootscreen {
"Click to Connect"
TR::homescreen__click_to_connect
} else {
"Click to Unlock"
TR::homescreen__click_to_unlock
};
Lockscreen {
Ok(Lockscreen {
label: Child::new(Label::centered(label, theme::TEXT_BIG)),
instruction: Child::new(Label::centered(instruction_str.into(), theme::TEXT_NORMAL)),
instruction: Child::new(Label::centered(
instruction_str.try_into()?,
theme::TEXT_NORMAL,
)),
invisible_buttons: Child::new(ButtonController::new(invisible_btn_layout)),
coinjoin_icon: coinjoin_authorized.then_some(theme::ICON_COINJOIN),
screensaver: !bootscreen,
}
})
}
}
@ -321,7 +328,7 @@ where
{
title: Child<Label<T>>,
buffer_func: F,
buttons: Child<ButtonController<T>>,
buttons: Child<ButtonController>,
}
impl<T, F> ConfirmHomescreen<T, F>
@ -329,7 +336,7 @@ where
T: StringType + Clone,
{
pub fn new(title: T, buffer_func: F) -> Self {
let btn_layout = ButtonLayout::cancel_none_text("CHANGE".into());
let btn_layout = ButtonLayout::cancel_none_text(TR::buttons__change.into());
ConfirmHomescreen {
title: Child::new(Label::centered(title, theme::TEXT_BOLD)),
buffer_func,

@ -1,10 +1,7 @@
use crate::{
strutil::StringType,
ui::{
component::{Child, Component, Event, EventCtx, Pad},
geometry::{Insets, Offset, Rect},
util::animation_disabled,
},
use crate::ui::{
component::{Child, Component, Event, EventCtx, Pad},
geometry::{Insets, Offset, Rect},
util::animation_disabled,
};
use super::super::{
@ -13,7 +10,7 @@ use super::super::{
const DEFAULT_ITEMS_DISTANCE: i16 = 10;
pub trait Choice<T: StringType> {
pub trait Choice {
// Only `paint_center` is required, the rest is optional
// and therefore has a default implementation.
fn paint_center(&self, area: Rect, inverse: bool);
@ -26,9 +23,7 @@ pub trait Choice<T: StringType> {
0
}
fn btn_layout(&self) -> ButtonLayout<T> {
ButtonLayout::default_three_icons()
}
fn btn_layout(&self) -> ButtonLayout;
/// Whether it is possible to do the middle action event without
/// releasing the button - after long-press duration is reached.
@ -46,9 +41,9 @@ pub trait Choice<T: StringType> {
/// 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> {
pub trait ChoiceFactory {
type Action;
type Item: Choice<T>;
type Item: Choice;
fn count(&self) -> usize;
fn get(&self, index: usize) -> (Self::Item, Self::Action);
@ -67,14 +62,13 @@ pub trait ChoiceFactory<T: StringType> {
///
/// `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>
pub struct ChoicePage<F, A>
where
F: ChoiceFactory<T, Action = A>,
T: StringType,
F: ChoiceFactory<Action = A>,
{
choices: F,
pad: Pad,
buttons: Child<ButtonController<T>>,
buttons: Child<ButtonController>,
page_counter: usize,
/// How many pixels are between the items.
items_distance: i16,
@ -97,10 +91,9 @@ where
animated_steps_to_do: i16,
}
impl<F, T, A> ChoicePage<F, T, A>
impl<F, A> ChoicePage<F, A>
where
F: ChoiceFactory<T, Action = A>,
T: StringType + Clone,
F: ChoiceFactory<Action = A>,
{
pub fn new(choices: F) -> Self {
let initial_btn_layout = choices.get(0).0.btn_layout();
@ -283,12 +276,12 @@ where
}
/// Getting the choice on the current index
fn get_current_choice(&self) -> (<F as ChoiceFactory<T>>::Item, A) {
fn get_current_choice(&self) -> (<F as ChoiceFactory>::Item, A) {
self.choices.get(self.page_counter)
}
/// Getting the current item
pub fn get_current_item(&self) -> <F as ChoiceFactory<T>>::Item {
pub fn get_current_item(&self) -> <F as ChoiceFactory>::Item {
self.get_current_choice().0
}
@ -484,10 +477,9 @@ where
}
}
impl<F, T, A> Component for ChoicePage<F, T, A>
impl<F, A> Component for ChoicePage<F, A>
where
F: ChoiceFactory<T, Action = A>,
T: StringType + Clone,
F: ChoiceFactory<Action = A>,
{
type Msg = (A, bool);
@ -599,11 +591,10 @@ where
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
impl<F, T, A> crate::trace::Trace for ChoicePage<F, T, A>
impl<F, A> crate::trace::Trace for ChoicePage<F, A>
where
F: ChoiceFactory<T, Action = A>,
T: StringType + Clone,
<F as ChoiceFactory<T>>::Item: crate::trace::Trace,
F: ChoiceFactory<Action = A>,
<F as ChoiceFactory>::Item: crate::trace::Trace,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("ChoicePage");

@ -1,5 +1,5 @@
use crate::{
strutil::{ShortString, StringType},
strutil::ShortString,
ui::{
display::{self, rect_fill, rect_fill_corners, rect_outline_rounded, Font, Icon},
geometry::{Alignment2D, Offset, Rect},
@ -14,16 +14,16 @@ const ICON_RIGHT_PADDING: i16 = 2;
/// Simple string component used as a choice item.
#[derive(Clone)]
pub struct ChoiceItem<T: StringType> {
pub struct ChoiceItem {
text: ShortString,
icon: Option<Icon>,
btn_layout: ButtonLayout<T>,
btn_layout: ButtonLayout,
font: Font,
middle_action_without_release: bool,
}
impl<T: StringType> ChoiceItem<T> {
pub fn new<U: AsRef<str>>(text: U, btn_layout: ButtonLayout<T>) -> Self {
impl ChoiceItem {
pub fn new<U: AsRef<str>>(text: U, btn_layout: ButtonLayout) -> Self {
Self {
text: String::from(text.as_ref()),
icon: None,
@ -55,17 +55,17 @@ impl<T: StringType> ChoiceItem<T> {
}
/// Setting left button.
pub fn set_left_btn(&mut self, btn_left: Option<ButtonDetails<T>>) {
pub fn set_left_btn(&mut self, btn_left: Option<ButtonDetails>) {
self.btn_layout.btn_left = btn_left;
}
/// Setting middle button.
pub fn set_middle_btn(&mut self, btn_middle: Option<ButtonDetails<T>>) {
pub fn set_middle_btn(&mut self, btn_middle: Option<ButtonDetails>) {
self.btn_layout.btn_middle = btn_middle;
}
/// Setting right button.
pub fn set_right_btn(&mut self, btn_right: Option<ButtonDetails<T>>) {
pub fn set_right_btn(&mut self, btn_right: Option<ButtonDetails>) {
self.btn_layout.btn_right = btn_right;
}
@ -87,10 +87,7 @@ impl<T: StringType> ChoiceItem<T> {
}
}
impl<T> Choice<T> for ChoiceItem<T>
where
T: StringType + Clone,
{
impl Choice for ChoiceItem {
/// 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) {
@ -125,7 +122,7 @@ where
}
/// Getting current button layout.
fn btn_layout(&self) -> ButtonLayout<T> {
fn btn_layout(&self) -> ButtonLayout {
self.btn_layout.clone()
}
@ -196,9 +193,9 @@ fn paint_text_icon(
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
impl<T: StringType> crate::trace::Trace for ChoiceItem<T> {
impl crate::trace::Trace for ChoiceItem {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("ChoiceItem");
t.string("content", self.text.as_ref());
t.string("content", self.text.as_str().into());
}
}

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

@ -1,5 +1,5 @@
use crate::{
strutil::StringType,
translations::TR,
ui::{
component::{Component, Event, EventCtx},
geometry::Rect,
@ -20,9 +20,9 @@ impl ChoiceFactoryNumberInput {
}
}
impl<T: StringType + Clone> ChoiceFactory<T> for ChoiceFactoryNumberInput {
impl ChoiceFactory for ChoiceFactoryNumberInput {
type Action = u32;
type Item = ChoiceItem<T>;
type Item = ChoiceItem;
fn count(&self) -> usize {
(self.max - self.min + 1) as usize
@ -31,14 +31,17 @@ impl<T: StringType + Clone> ChoiceFactory<T> for ChoiceFactoryNumberInput {
fn get(&self, choice_index: usize) -> (Self::Item, Self::Action) {
let num = self.min + choice_index as u32;
let text: String<10> = String::from(num);
let mut choice_item = ChoiceItem::new(text, ButtonLayout::default_three_icons());
let mut choice_item = ChoiceItem::new(
text,
ButtonLayout::arrow_armed_arrow(TR::buttons__select.into()),
);
// Disabling prev/next buttons for the first/last choice.
// (could be done to the same button if there is only one)
if choice_index == 0 {
choice_item.set_left_btn(None);
}
if choice_index == <ChoiceFactoryNumberInput as ChoiceFactory<T>>::count(self) - 1 {
if choice_index == <ChoiceFactoryNumberInput as ChoiceFactory>::count(self) - 1 {
choice_item.set_right_btn(None);
}
@ -48,15 +51,12 @@ impl<T: StringType + Clone> ChoiceFactory<T> for ChoiceFactoryNumberInput {
/// Simple wrapper around `ChoicePage` that allows for
/// inputting a list of values and receiving the chosen one.
pub struct NumberInput<T: StringType + Clone> {
choice_page: ChoicePage<ChoiceFactoryNumberInput, T, u32>,
pub struct NumberInput {
choice_page: ChoicePage<ChoiceFactoryNumberInput, u32>,
min: u32,
}
impl<T> NumberInput<T>
where
T: StringType + Clone,
{
impl NumberInput {
pub fn new(min: u32, max: u32, init_value: u32) -> Self {
let choices = ChoiceFactoryNumberInput::new(min, max);
let initial_page = init_value - min;
@ -67,10 +67,7 @@ where
}
}
impl<T> Component for NumberInput<T>
where
T: StringType + Clone,
{
impl Component for NumberInput {
type Msg = u32;
fn place(&mut self, bounds: Rect) -> Rect {
@ -89,10 +86,7 @@ where
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for NumberInput<T>
where
T: StringType + Clone,
{
impl crate::trace::Trace for NumberInput {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("NumberInput");
t.child("choice_page", &self.choice_page);

@ -1,5 +1,6 @@
use crate::{
strutil::StringType,
strutil::TString,
translations::TR,
trezorhal::random,
ui::{
component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx},
@ -43,64 +44,72 @@ const DIGITS_INDEX: usize = 5;
const SPECIAL_INDEX: usize = 6;
const SPACE_INDEX: usize = 7;
/// Menu text, action, icon data, middle button with CONFIRM, without_release
const MENU: [(&str, PassphraseAction, Option<Icon>, bool, bool); MENU_LENGTH] = [
(
"SHOW",
PassphraseAction::Show,
Some(theme::ICON_EYE),
true,
false,
),
(
"CANCEL_OR_DELETE", // will be chosen dynamically
PassphraseAction::CancelOrDelete,
None,
true,
true, // without_release
),
(
"ENTER",
PassphraseAction::Enter,
Some(theme::ICON_TICK),
true,
false,
),
(
"abc",
PassphraseAction::Category(ChoiceCategory::LowercaseLetter),
None,
false,
false,
),
(
"ABC",
PassphraseAction::Category(ChoiceCategory::UppercaseLetter),
None,
false,
false,
),
(
"123",
PassphraseAction::Category(ChoiceCategory::Digit),
None,
false,
false,
),
(
"#$!",
PassphraseAction::Category(ChoiceCategory::SpecialSymbol),
None,
false,
false,
),
(
"SPACE",
PassphraseAction::Character(' '),
Some(theme::ICON_SPACE),
false,
false,
),
#[derive(Clone)]
struct MenuItem {
text: TString<'static>,
action: PassphraseAction,
icon: Option<Icon>,
show_confirm: bool,
without_release: bool,
}
const MENU: [MenuItem; MENU_LENGTH] = [
MenuItem {
text: TR::inputs__show.as_tstring(),
action: PassphraseAction::Show,
icon: Some(theme::ICON_EYE),
show_confirm: true,
without_release: false,
},
MenuItem {
text: TString::from_str("CANCEL OR DELETE"),
action: PassphraseAction::CancelOrDelete,
icon: None,
show_confirm: true,
without_release: true,
},
MenuItem {
text: TR::inputs__enter.as_tstring(),
action: PassphraseAction::Enter,
icon: Some(theme::ICON_TICK),
show_confirm: true,
without_release: false,
},
MenuItem {
text: TString::from_str("abc"),
action: PassphraseAction::Category(ChoiceCategory::LowercaseLetter),
icon: None,
show_confirm: false,
without_release: false,
},
MenuItem {
text: TString::from_str("ABC"),
action: PassphraseAction::Category(ChoiceCategory::UppercaseLetter),
icon: None,
show_confirm: false,
without_release: false,
},
MenuItem {
text: TString::from_str("123"),
action: PassphraseAction::Category(ChoiceCategory::Digit),
icon: None,
show_confirm: false,
without_release: false,
},
MenuItem {
text: TString::from_str("#$!"),
action: PassphraseAction::Category(ChoiceCategory::SpecialSymbol),
icon: None,
show_confirm: false,
without_release: false,
},
MenuItem {
text: TR::inputs__space.as_tstring(),
action: PassphraseAction::Character(' '),
icon: Some(theme::ICON_SPACE),
show_confirm: false,
without_release: false,
},
];
#[derive(Clone, Copy)]
@ -172,66 +181,74 @@ impl ChoiceFactoryPassphrase {
}
/// MENU choices with accept and cancel hold-to-confirm side buttons.
fn get_menu_item<T: StringType>(
&self,
choice_index: usize,
) -> (ChoiceItem<T>, PassphraseAction) {
fn get_menu_item(&self, choice_index: usize) -> (ChoiceItem, PassphraseAction) {
#[allow(const_item_mutation)]
let current_item = &mut MENU[choice_index];
// More options for CANCEL/DELETE button
let (mut text, action, mut icon, show_confirm, without_release) = MENU[choice_index];
if matches!(action, PassphraseAction::CancelOrDelete) {
if matches!(current_item.action, PassphraseAction::CancelOrDelete) {
if self.is_empty {
text = "CANCEL";
icon = Some(theme::ICON_CANCEL);
current_item.text = TR::inputs__cancel.into();
current_item.icon = Some(theme::ICON_CANCEL);
} else {
text = "DELETE";
icon = Some(theme::ICON_DELETE);
current_item.text = TR::inputs__delete.into();
current_item.icon = Some(theme::ICON_DELETE);
}
}
let mut menu_item = ChoiceItem::new(text, ButtonLayout::default_three_icons());
let mut menu_item = current_item.text.map(|t| {
ChoiceItem::new(
t,
ButtonLayout::arrow_armed_arrow(TR::buttons__select.into()),
)
});
// Action buttons have different middle button text
if show_confirm {
let confirm_btn = ButtonDetails::armed_text("CONFIRM".into());
if current_item.show_confirm {
let confirm_btn = ButtonDetails::armed_text(TR::buttons__confirm.into());
menu_item.set_middle_btn(Some(confirm_btn));
}
// Making middle button create LongPress events
if without_release {
if current_item.without_release {
menu_item = menu_item.with_middle_action_without_release();
}
if let Some(icon) = icon {
if let Some(icon) = current_item.icon {
menu_item = menu_item.with_icon(icon);
}
(menu_item, action)
(menu_item, current_item.action)
}
/// Character choices with a BACK to MENU choice at the end (visible from
/// start) to return back
fn get_character_item<T: StringType>(
&self,
choice_index: usize,
) -> (ChoiceItem<T>, PassphraseAction) {
fn get_character_item(&self, choice_index: usize) -> (ChoiceItem, PassphraseAction) {
if is_menu_choice(&self.current_category, choice_index) {
(
ChoiceItem::new("BACK", ButtonLayout::arrow_armed_arrow("RETURN".into()))
.with_icon(theme::ICON_ARROW_BACK_UP),
TR::inputs__back.map_translated(|t| {
ChoiceItem::new(
t,
ButtonLayout::arrow_armed_arrow(TR::inputs__return.into()),
)
.with_icon(theme::ICON_ARROW_BACK_UP)
}),
PassphraseAction::Menu,
)
} else {
let ch = get_char(&self.current_category, choice_index);
(
ChoiceItem::new(char_to_string(ch), ButtonLayout::default_three_icons()),
ChoiceItem::new(
char_to_string(ch),
ButtonLayout::arrow_armed_arrow(TR::buttons__select.into()),
),
PassphraseAction::Character(ch),
)
}
}
}
impl<T: StringType + Clone> ChoiceFactory<T> for ChoiceFactoryPassphrase {
impl ChoiceFactory for ChoiceFactoryPassphrase {
type Action = PassphraseAction;
type Item = ChoiceItem<T>;
type Item = ChoiceItem;
fn count(&self) -> usize {
let length = get_category_length(&self.current_category);
@ -250,8 +267,8 @@ impl<T: StringType + Clone> ChoiceFactory<T> for ChoiceFactoryPassphrase {
}
/// Component for entering a passphrase.
pub struct PassphraseEntry<T: StringType + Clone> {
choice_page: ChoicePage<ChoiceFactoryPassphrase, T, PassphraseAction>,
pub struct PassphraseEntry {
choice_page: ChoicePage<ChoiceFactoryPassphrase, PassphraseAction>,
passphrase_dots: Child<ChangingTextLine<String<MAX_PASSPHRASE_LENGTH>>>,
show_plain_passphrase: bool,
show_last_digit: bool,
@ -259,10 +276,7 @@ pub struct PassphraseEntry<T: StringType + Clone> {
current_category: ChoiceCategory,
}
impl<T> PassphraseEntry<T>
where
T: StringType + Clone,
{
impl PassphraseEntry {
pub fn new() -> Self {
Self {
choice_page: ChoicePage::new(ChoiceFactoryPassphrase::new(ChoiceCategory::Menu, true))
@ -353,10 +367,7 @@ where
}
}
impl<T> Component for PassphraseEntry<T>
where
T: StringType + Clone,
{
impl Component for PassphraseEntry {
type Msg = CancelConfirmMsg;
fn place(&mut self, bounds: Rect) -> Rect {
@ -442,21 +453,18 @@ where
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for PassphraseEntry<T>
where
T: StringType + Clone,
{
impl crate::trace::Trace for PassphraseEntry {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("PassphraseKeyboard");
t.string("passphrase", self.textbox.content());
t.string("passphrase", self.textbox.content().into());
t.string(
"current_category",
match self.current_category {
ChoiceCategory::Menu => "MENU",
ChoiceCategory::LowercaseLetter => MENU[LOWERCASE_INDEX].0,
ChoiceCategory::UppercaseLetter => MENU[UPPERCASE_INDEX].0,
ChoiceCategory::Digit => MENU[DIGITS_INDEX].0,
ChoiceCategory::SpecialSymbol => MENU[SPECIAL_INDEX].0,
ChoiceCategory::Menu => "MENU".into(),
ChoiceCategory::LowercaseLetter => MENU[LOWERCASE_INDEX].text,
ChoiceCategory::UppercaseLetter => MENU[UPPERCASE_INDEX].text,
ChoiceCategory::Digit => MENU[DIGITS_INDEX].text,
ChoiceCategory::SpecialSymbol => MENU[SPECIAL_INDEX].text,
},
);
t.child("choice_page", &self.choice_page);

@ -1,5 +1,6 @@
use crate::{
strutil::StringType,
strutil::{StringType, TString},
translations::TR,
trezorhal::random,
ui::{
component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx},
@ -22,27 +23,65 @@ enum PinAction {
Digit(char),
}
struct PinChoice {
text: TString<'static>,
action: PinAction,
icon: Option<Icon>,
without_release: bool,
}
impl PinChoice {
pub const fn new(
text: TString<'static>,
action: PinAction,
icon: Option<Icon>,
without_release: bool,
) -> Self {
Self {
text,
action,
icon,
without_release,
}
}
}
const MAX_PIN_LENGTH: usize = 50;
const EMPTY_PIN_STR: &str = "_";
const CHOICE_LENGTH: usize = 13;
const NUMBER_START_INDEX: usize = 3;
/// Text, action, icon, without_release
const CHOICES: [(&str, PinAction, Option<Icon>, bool); CHOICE_LENGTH] = [
const CHOICES: [PinChoice; CHOICE_LENGTH] = [
// DELETE should be triggerable without release (after long-press)
("DELETE", PinAction::Delete, Some(theme::ICON_DELETE), true),
("SHOW", PinAction::Show, Some(theme::ICON_EYE), false),
("ENTER", PinAction::Enter, Some(theme::ICON_TICK), false),
("0", PinAction::Digit('0'), None, false),
("1", PinAction::Digit('1'), None, false),
("2", PinAction::Digit('2'), None, false),
("3", PinAction::Digit('3'), None, false),
("4", PinAction::Digit('4'), None, false),
("5", PinAction::Digit('5'), None, false),
("6", PinAction::Digit('6'), None, false),
("7", PinAction::Digit('7'), None, false),
("8", PinAction::Digit('8'), None, false),
("9", PinAction::Digit('9'), None, false),
PinChoice::new(
TR::inputs__delete.as_tstring(),
PinAction::Delete,
Some(theme::ICON_DELETE),
true, // without_release
),
PinChoice::new(
TR::inputs__show.as_tstring(),
PinAction::Show,
Some(theme::ICON_EYE),
false,
),
PinChoice::new(
TR::inputs__enter.as_tstring(),
PinAction::Enter,
Some(theme::ICON_TICK),
false,
),
PinChoice::new(TString::from_str("0"), PinAction::Digit('0'), None, false),
PinChoice::new(TString::from_str("1"), PinAction::Digit('1'), None, false),
PinChoice::new(TString::from_str("2"), PinAction::Digit('2'), None, false),
PinChoice::new(TString::from_str("3"), PinAction::Digit('3'), None, false),
PinChoice::new(TString::from_str("4"), PinAction::Digit('4'), None, false),
PinChoice::new(TString::from_str("5"), PinAction::Digit('5'), None, false),
PinChoice::new(TString::from_str("6"), PinAction::Digit('6'), None, false),
PinChoice::new(TString::from_str("7"), PinAction::Digit('7'), None, false),
PinChoice::new(TString::from_str("8"), PinAction::Digit('8'), None, false),
PinChoice::new(TString::from_str("9"), PinAction::Digit('9'), None, false),
];
fn get_random_digit_position() -> usize {
@ -51,32 +90,37 @@ fn get_random_digit_position() -> usize {
struct ChoiceFactoryPIN;
impl<T: StringType + Clone> ChoiceFactory<T> for ChoiceFactoryPIN {
impl ChoiceFactory for ChoiceFactoryPIN {
type Action = PinAction;
type Item = ChoiceItem<T>;
type Item = ChoiceItem;
fn get(&self, choice_index: usize) -> (Self::Item, Self::Action) {
let (choice_str, action, icon, without_release) = CHOICES[choice_index];
let choice = &CHOICES[choice_index];
let mut choice_item = ChoiceItem::new(choice_str, ButtonLayout::default_three_icons());
let mut choice_item = choice.text.map(|t| {
ChoiceItem::new(
t,
ButtonLayout::arrow_armed_arrow(TR::buttons__select.into()),
)
});
// Action buttons have different middle button text
if !matches!(action, PinAction::Digit(_)) {
let confirm_btn = ButtonDetails::armed_text("CONFIRM".into());
if !matches!(choice.action, PinAction::Digit(_)) {
let confirm_btn = ButtonDetails::armed_text(TR::buttons__confirm.into());
choice_item.set_middle_btn(Some(confirm_btn));
}
// Making middle button create LongPress events
if without_release {
if choice.without_release {
choice_item = choice_item.with_middle_action_without_release();
}
// Adding icons for appropriate items
if let Some(icon) = icon {
if let Some(icon) = choice.icon {
choice_item = choice_item.with_icon(icon);
}
(choice_item, action)
(choice_item, choice.action)
}
fn count(&self) -> usize {
@ -86,7 +130,7 @@ impl<T: StringType + Clone> ChoiceFactory<T> for ChoiceFactoryPIN {
/// Component for entering a PIN.
pub struct PinEntry<T: StringType + Clone> {
choice_page: ChoicePage<ChoiceFactoryPIN, T, PinAction>,
choice_page: ChoicePage<ChoiceFactoryPIN, PinAction>,
header_line: Child<ChangingTextLine<String<MAX_PIN_LENGTH>>>,
pin_line: Child<ChangingTextLine<String<MAX_PIN_LENGTH>>>,
prompt: T,
@ -111,7 +155,7 @@ where
let (showing_real_prompt, header_line_content, pin_line_content) = if show_subprompt {
(
false,
String::from("WRONG PIN"),
TR::pin__title_wrong_pin.map_translated(|t| String::from(t)),
String::from(subprompt.as_ref()),
)
} else {
@ -296,8 +340,8 @@ where
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("PinKeyboard");
t.string("subprompt", self.subprompt.as_ref());
t.string("pin", self.textbox.content());
t.string("subprompt", self.subprompt.as_ref().into());
t.string("pin", self.textbox.content().into());
t.child("choice_page", &self.choice_page);
}
}

@ -1,5 +1,6 @@
use crate::{
strutil::StringType,
strutil::TString,
translations::TR,
ui::{
component::{Component, Event, EventCtx},
geometry::Rect,
@ -13,24 +14,24 @@ use heapless::Vec;
// as would be via `const N: usize` generics.
const MAX_LENGTH: usize = 5;
struct ChoiceFactorySimple<T: StringType> {
choices: Vec<T, MAX_LENGTH>,
struct ChoiceFactorySimple {
choices: Vec<TString<'static>, MAX_LENGTH>,
carousel: bool,
}
impl<T: StringType> ChoiceFactorySimple<T> {
fn new(choices: Vec<T, MAX_LENGTH>, carousel: bool) -> Self {
impl ChoiceFactorySimple {
fn new(choices: Vec<TString<'static>, MAX_LENGTH>, carousel: bool) -> Self {
Self { choices, carousel }
}
fn get_string(&self, choice_index: usize) -> &str {
self.choices[choice_index].as_ref()
fn get_string(&self, choice_index: usize) -> TString<'static> {
self.choices[choice_index]
}
}
impl<T: StringType + Clone> ChoiceFactory<T> for ChoiceFactorySimple<T> {
impl ChoiceFactory for ChoiceFactorySimple {
type Action = usize;
type Item = ChoiceItem<T>;
type Item = ChoiceItem;
fn count(&self) -> usize {
self.choices.len()
@ -38,7 +39,12 @@ impl<T: StringType + Clone> ChoiceFactory<T> for ChoiceFactorySimple<T> {
fn get(&self, choice_index: usize) -> (Self::Item, Self::Action) {
let text = &self.choices[choice_index];
let mut choice_item = ChoiceItem::new(text, ButtonLayout::default_three_icons());
let mut choice_item = text.map(|t| {
ChoiceItem::new(
t,
ButtonLayout::arrow_armed_arrow(TR::buttons__select.into()),
)
});
// Disabling prev/next buttons for the first/last choice when not in carousel.
// (could be done to the same button if there is only one)
@ -57,19 +63,13 @@ impl<T: StringType + Clone> ChoiceFactory<T> for ChoiceFactorySimple<T> {
/// Simple wrapper around `ChoicePage` that allows for
/// inputting a list of values and receiving the chosen one.
pub struct SimpleChoice<T>
where
T: StringType + Clone,
{
choice_page: ChoicePage<ChoiceFactorySimple<T>, T, usize>,
pub struct SimpleChoice {
choice_page: ChoicePage<ChoiceFactorySimple, usize>,
pub return_index: bool,
}
impl<T> SimpleChoice<T>
where
T: StringType + Clone,
{
pub fn new(str_choices: Vec<T, MAX_LENGTH>, carousel: bool) -> Self {
impl SimpleChoice {
pub fn new(str_choices: Vec<TString<'static>, MAX_LENGTH>, carousel: bool) -> Self {
let choices = ChoiceFactorySimple::new(str_choices, carousel);
Self {
choice_page: ChoicePage::new(choices).with_carousel(carousel),
@ -96,15 +96,12 @@ where
}
/// Translating the resulting index into actual string choice.
pub fn result_by_index(&self, index: usize) -> &str {
pub fn result_by_index(&self, index: usize) -> TString<'static> {
self.choice_page.choice_factory().get_string(index)
}
}
impl<T> Component for SimpleChoice<T>
where
T: StringType + Clone,
{
impl Component for SimpleChoice {
type Msg = usize;
fn place(&mut self, bounds: Rect) -> Rect {
@ -123,10 +120,7 @@ where
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for SimpleChoice<T>
where
T: StringType + Clone,
{
impl crate::trace::Trace for SimpleChoice {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("SimpleChoice");
t.child("choice_page", &self.choice_page);

@ -1,5 +1,5 @@
use crate::{
strutil::StringType,
translations::TR,
trezorhal::{random, wordlist::Wordlist},
ui::{
component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx},
@ -76,9 +76,9 @@ impl ChoiceFactoryWordlist {
}
}
impl<T: StringType + Clone> ChoiceFactory<T> for ChoiceFactoryWordlist {
impl ChoiceFactory for ChoiceFactoryWordlist {
type Action = WordlistAction;
type Item = ChoiceItem<T>;
type Item = ChoiceItem;
fn count(&self) -> usize {
// Accounting for the DELETE option (+1)
@ -94,9 +94,14 @@ impl<T: StringType + Clone> ChoiceFactory<T> for ChoiceFactoryWordlist {
// (is a requirement for WORDS, doing it for LETTERS as well to unite it)
if choice_index == DELETE_INDEX {
return (
ChoiceItem::new("DELETE", ButtonLayout::arrow_armed_arrow("CONFIRM".into()))
TR::inputs__delete.map_translated(|t| {
ChoiceItem::new(
t,
ButtonLayout::arrow_armed_arrow(TR::buttons__confirm.into()),
)
.with_icon(theme::ICON_DELETE)
.with_middle_action_without_release(),
.with_middle_action_without_release()
}),
WordlistAction::Delete,
);
}
@ -105,7 +110,10 @@ impl<T: StringType + Clone> ChoiceFactory<T> for ChoiceFactoryWordlist {
let index = self.word_random_order[choice_index - 1];
let word = self.wordlist.get(index).unwrap_or_default();
(
ChoiceItem::new(word, ButtonLayout::default_three_icons()),
ChoiceItem::new(
word,
ButtonLayout::arrow_armed_arrow(TR::buttons__select.into()),
),
WordlistAction::Word(word),
)
} else {
@ -115,7 +123,10 @@ impl<T: StringType + Clone> ChoiceFactory<T> for ChoiceFactoryWordlist {
.nth(choice_index - 1)
.unwrap_or_default();
(
ChoiceItem::new(char_to_string(letter), ButtonLayout::default_three_icons()),
ChoiceItem::new(
char_to_string(letter),
ButtonLayout::arrow_armed_arrow(TR::buttons__select.into()),
),
WordlistAction::Letter(letter),
)
}
@ -123,21 +134,18 @@ impl<T: StringType + Clone> ChoiceFactory<T> for ChoiceFactoryWordlist {
}
/// Component for entering a mnemonic from a wordlist - BIP39 or SLIP39.
pub struct WordlistEntry<T: StringType + Clone> {
choice_page: ChoicePage<ChoiceFactoryWordlist, T, WordlistAction>,
pub struct WordlistEntry {
choice_page: ChoicePage<ChoiceFactoryWordlist, WordlistAction>,
chosen_letters: Child<ChangingTextLine<String<{ MAX_WORD_LENGTH + 1 }>>>,
textbox: TextBox<MAX_WORD_LENGTH>,
offer_words: bool,
wordlist_type: WordlistType,
}
impl<T> WordlistEntry<T>
where
T: StringType + Clone,
{
impl WordlistEntry {
pub fn new(wordlist_type: WordlistType) -> Self {
let choices = ChoiceFactoryWordlist::new(wordlist_type, "");
let choices_count = <ChoiceFactoryWordlist as ChoiceFactory<T>>::count(&choices);
let choices_count = <ChoiceFactoryWordlist as ChoiceFactory>::count(&choices);
Self {
// Starting at random letter position
choice_page: ChoicePage::new(choices)
@ -167,7 +175,7 @@ where
if self.offer_words {
INITIAL_PAGE_COUNTER
} else {
let choices_count = <ChoiceFactoryWordlist as ChoiceFactory<T>>::count(new_choices);
let choices_count = <ChoiceFactoryWordlist as ChoiceFactory>::count(new_choices);
// There should be always DELETE and at least one letter
assert!(choices_count > 1);
if choices_count == 2 {
@ -179,8 +187,7 @@ where
loop {
let random_position = get_random_position(choices_count);
let current_action =
<ChoiceFactoryWordlist as ChoiceFactory<T>>::get(new_choices, random_position)
.1;
<ChoiceFactoryWordlist as ChoiceFactory>::get(new_choices, random_position).1;
if let WordlistAction::Letter(current_letter) = current_action {
if let Some(last_letter) = self.get_last_textbox_letter() {
if current_letter == last_letter {
@ -217,10 +224,7 @@ where
}
}
impl<T> Component for WordlistEntry<T>
where
T: StringType + Clone,
{
impl Component for WordlistEntry {
type Msg = &'static str;
fn place(&mut self, bounds: Rect) -> Rect {
@ -264,13 +268,10 @@ where
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for WordlistEntry<T>
where
T: StringType + Clone,
{
impl crate::trace::Trace for WordlistEntry {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("MnemonicKeyboard"); // unified with TT
t.string("textbox", self.textbox.content());
t.string("textbox", self.textbox.content().into());
if self.offer_words {
t.bool("word_choices", true);

@ -1,5 +1,5 @@
use crate::{
strutil::StringType,
strutil::{StringType, TString},
time::{Duration, Instant},
ui::{
animation::Animation,
@ -28,25 +28,19 @@ enum State {
Grown,
}
pub struct Loader<T>
where
T: StringType,
{
pub struct Loader {
area: Rect,
state: State,
growing_duration: Duration,
shrinking_duration: Duration,
text_overlay: display::TextOverlay<T>,
text_overlay: display::TextOverlay,
styles: LoaderStyleSheet,
}
impl<T> Loader<T>
where
T: StringType,
{
impl Loader {
pub const SIZE: Offset = Offset::new(120, 120);
pub fn new(text_overlay: display::TextOverlay<T>, styles: LoaderStyleSheet) -> Self {
pub fn new(text_overlay: display::TextOverlay, styles: LoaderStyleSheet) -> Self {
Self {
area: Rect::zero(),
state: State::Initial,
@ -57,8 +51,8 @@ where
}
}
pub fn text(text: T, styles: LoaderStyleSheet) -> Self {
let text_overlay = display::TextOverlay::new(text, styles.normal.font);
pub fn text<T: Into<TString<'static>>>(text: T, styles: LoaderStyleSheet) -> Self {
let text_overlay = display::TextOverlay::new(text.into(), styles.normal.font);
Self::new(text_overlay, styles)
}
@ -77,18 +71,18 @@ where
self.growing_duration
}
pub fn get_text(&self) -> &T {
pub fn get_text(&self) -> TString<'static> {
self.text_overlay.get_text()
}
/// Change the text of the loader.
pub fn set_text(&mut self, text: T) {
self.text_overlay.set_text(text);
pub fn set_text<T: Into<TString<'static>>>(&mut self, text: T) {
self.text_overlay.set_text(text.into());
}
/// Return width of given text according to current style.
pub fn get_text_width(&self, text: &T) -> i16 {
self.styles.normal.font.text_width(text.as_ref())
pub fn get_text_width(&self, text: &TString<'static>) -> i16 {
text.map(|t| self.styles.normal.font.text_width(t))
}
pub fn start_growing(&mut self, ctx: &mut EventCtx, now: Instant) {
@ -172,10 +166,7 @@ where
}
}
impl<T> Component for Loader<T>
where
T: StringType,
{
impl Component for Loader {
type Msg = LoaderMsg;
fn place(&mut self, bounds: Rect) -> Rect {
@ -346,13 +337,10 @@ where
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for Loader<T>
where
T: StringType,
{
impl crate::trace::Trace for Loader {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("Loader");
t.string("text", self.get_text().as_ref());
t.string("text", self.get_text());
t.int("duration", self.get_duration().to_millis() as i64);
}
}

@ -26,32 +26,39 @@ pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet, ProgressLoade
pub use result::ResultScreen;
pub use welcome_screen::WelcomeScreen;
#[cfg(feature = "translations")]
mod address_details;
mod changing_text;
#[cfg(feature = "translations")]
mod coinjoin_progress;
mod flow;
mod flow_pages;
mod frame;
#[cfg(feature = "micropython")]
mod homescreen;
#[cfg(feature = "translations")]
mod page;
mod progress;
mod result_anim;
mod result_popup;
mod scrollbar;
#[cfg(feature = "translations")]
mod share_words;
mod show_more;
mod title;
#[cfg(feature = "translations")]
pub use address_details::AddressDetails;
pub use changing_text::ChangingTextLine;
#[cfg(feature = "translations")]
pub use coinjoin_progress::CoinJoinProgress;
pub use flow::Flow;
pub use flow_pages::{FlowPages, Page};
pub use frame::{Frame, ScrollableContent, ScrollableFrame};
#[cfg(feature = "micropython")]
pub use homescreen::{check_homescreen_format, ConfirmHomescreen, Homescreen, Lockscreen};
#[cfg(feature = "translations")]
pub use input_methods::{
number_input::NumberInput,
passphrase::PassphraseEntry,
@ -59,10 +66,12 @@ pub use input_methods::{
simple_choice::SimpleChoice,
wordlist::{WordlistEntry, WordlistType},
};
#[cfg(feature = "translations")]
pub use page::ButtonPage;
pub use progress::Progress;
pub use result_anim::{ResultAnim, ResultAnimMsg};
pub use result_popup::{ResultPopup, ResultPopupMsg};
pub use scrollbar::ScrollBar;
#[cfg(feature = "translations")]
pub use share_words::ShareWords;
pub use show_more::{CancelInfoConfirmMsg, ShowMore};

@ -1,5 +1,5 @@
use crate::{
strutil::StringType,
translations::TR,
ui::{
component::{Child, Component, ComponentExt, Event, EventCtx, Pad, PageMsg, Paginate},
display::Color,
@ -12,30 +12,28 @@ use super::{
ButtonDetails, ButtonLayout, ButtonPos,
};
pub struct ButtonPage<T, U>
pub struct ButtonPage<T>
where
T: Component + Paginate,
U: StringType,
{
page_count: usize,
active_page: usize,
content: Child<T>,
pad: Pad,
/// Left button of the first screen
cancel_btn_details: Option<ButtonDetails<U>>,
cancel_btn_details: Option<ButtonDetails>,
/// Right button of the last screen
confirm_btn_details: Option<ButtonDetails<U>>,
confirm_btn_details: Option<ButtonDetails>,
/// Left button of every screen
back_btn_details: Option<ButtonDetails<U>>,
back_btn_details: Option<ButtonDetails>,
/// Right button of every screen apart the last one
next_btn_details: Option<ButtonDetails<U>>,
buttons: Child<ButtonController<U>>,
next_btn_details: Option<ButtonDetails>,
buttons: Child<ButtonController>,
}
impl<T, U> ButtonPage<T, U>
impl<T> ButtonPage<T>
where
T: Component + Paginate,
U: StringType + Clone,
{
pub fn new(content: T, background: Color) -> Self {
Self {
@ -44,7 +42,7 @@ where
content: Child::new(content),
pad: Pad::with_background(background).with_clear(),
cancel_btn_details: Some(ButtonDetails::cancel_icon()),
confirm_btn_details: Some(ButtonDetails::text("CONFIRM".into())),
confirm_btn_details: Some(ButtonDetails::text(TR::buttons__confirm.into())),
back_btn_details: Some(ButtonDetails::up_arrow_icon()),
next_btn_details: Some(ButtonDetails::down_arrow_icon_wide()),
// Setting empty layout for now, we do not yet know the page count.
@ -54,22 +52,22 @@ where
}
}
pub fn with_cancel_btn(mut self, btn_details: Option<ButtonDetails<U>>) -> Self {
pub fn with_cancel_btn(mut self, btn_details: Option<ButtonDetails>) -> Self {
self.cancel_btn_details = btn_details;
self
}
pub fn with_confirm_btn(mut self, btn_details: Option<ButtonDetails<U>>) -> Self {
pub fn with_confirm_btn(mut self, btn_details: Option<ButtonDetails>) -> Self {
self.confirm_btn_details = btn_details;
self
}
pub fn with_back_btn(mut self, btn_details: Option<ButtonDetails<U>>) -> Self {
pub fn with_back_btn(mut self, btn_details: Option<ButtonDetails>) -> Self {
self.back_btn_details = btn_details;
self
}
pub fn with_next_btn(mut self, btn_details: Option<ButtonDetails<U>>) -> Self {
pub fn with_next_btn(mut self, btn_details: Option<ButtonDetails>) -> Self {
self.next_btn_details = btn_details;
self
}
@ -118,14 +116,14 @@ where
});
}
fn get_button_layout(&self, has_prev: bool, has_next: bool) -> ButtonLayout<U> {
fn get_button_layout(&self, has_prev: bool, has_next: bool) -> ButtonLayout {
let btn_left = self.get_left_button_details(!has_prev);
let btn_right = self.get_right_button_details(has_next);
ButtonLayout::new(btn_left, None, btn_right)
}
/// Get the left button details, depending whether the page is first or not.
fn get_left_button_details(&self, is_first: bool) -> Option<ButtonDetails<U>> {
fn get_left_button_details(&self, is_first: bool) -> Option<ButtonDetails> {
if is_first {
self.cancel_btn_details.clone()
} else {
@ -135,7 +133,7 @@ where
/// Get the right button details, depending on whether there is a next
/// page.
fn get_right_button_details(&self, has_next_page: bool) -> Option<ButtonDetails<U>> {
fn get_right_button_details(&self, has_next_page: bool) -> Option<ButtonDetails> {
if has_next_page {
self.next_btn_details.clone()
} else {
@ -144,10 +142,9 @@ where
}
}
impl<T, U> ScrollableContent for ButtonPage<T, U>
impl<T> ScrollableContent for ButtonPage<T>
where
T: Component + Paginate,
U: StringType,
{
fn page_count(&self) -> usize {
self.page_count
@ -157,10 +154,9 @@ where
}
}
impl<T, U> Component for ButtonPage<T, U>
impl<T> Component for ButtonPage<T>
where
T: Component + Paginate,
U: StringType + Clone,
{
type Msg = PageMsg<T::Msg>;
@ -224,10 +220,9 @@ where
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
impl<T, U> crate::trace::Trace for ButtonPage<T, U>
impl<T> crate::trace::Trace for ButtonPage<T>
where
T: crate::trace::Trace + Paginate + Component,
U: StringType + Clone,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("ButtonPage");

@ -30,7 +30,7 @@ where
result_anim: Child<ResultAnim>,
headline: Option<Label<&'static str>>,
text: Child<Paragraphs<Paragraph<T>>>,
buttons: Option<Child<ButtonController<T>>>,
buttons: Option<Child<ButtonController>>,
autoclose: bool,
}

@ -1,5 +1,6 @@
use crate::{
strutil::StringType,
translations::TR,
ui::{
component::{
text::util::text_multiline, Child, Component, Event, EventCtx, Never, Paginate,
@ -69,19 +70,24 @@ where
}
fn get_final_text(&self) -> String<50> {
build_string!(
50,
"I wrote down all ",
inttostr!(self.share_words.len() as u8),
" words in order."
)
TR::share_words__wrote_down_all.map_translated(|wrote_down_all| {
TR::share_words__words_in_order.map_translated(|in_order| {
build_string!(
50,
wrote_down_all,
inttostr!(self.share_words.len() as u8),
in_order
)
})
})
}
/// Display the final page with user confirmation.
fn paint_final_page(&mut self) {
let final_text = self.get_final_text();
text_multiline(
self.area.split_top(INFO_TOP_OFFSET).1,
&self.get_final_text(),
final_text.as_str().into(),
Font::NORMAL,
theme::FG,
theme::BG,
@ -103,7 +109,7 @@ where
let baseline = self.area.top_left() + Offset::y(y_offset);
let ordinal = build_string!(5, inttostr!(index as u8 + 1), ".");
display_left(baseline + Offset::x(NUMBER_X_OFFSET), &ordinal, NUMBER_FONT);
display_left(baseline + Offset::x(WORD_X_OFFSET), &word, WORD_FONT);
display_left(baseline + Offset::x(WORD_X_OFFSET), word, WORD_FONT);
}
}
}
@ -183,6 +189,6 @@ where
}
content
};
t.string("screen_content", &content);
t.string("screen_content", content.as_str().into());
}
}

@ -1,5 +1,5 @@
use crate::{
strutil::StringType,
strutil::TString,
ui::{
component::{Child, Component, Event, EventCtx},
geometry::{Insets, Rect},
@ -14,20 +14,20 @@ pub enum CancelInfoConfirmMsg {
Confirmed,
}
pub struct ShowMore<T, U>
where
U: StringType,
{
pub struct ShowMore<T> {
content: Child<T>,
buttons: Child<ButtonController<U>>,
buttons: Child<ButtonController>,
}
impl<T, U> ShowMore<T, U>
impl<T> ShowMore<T>
where
T: Component,
U: StringType + Clone,
{
pub fn new(content: T, cancel_button: Option<U>, button: U) -> Self {
pub fn new(
content: T,
cancel_button: Option<TString<'static>>,
button: TString<'static>,
) -> Self {
let btn_layout = if let Some(cancel_text) = cancel_button {
ButtonLayout::text_armed_info(cancel_text, button)
} else {
@ -40,10 +40,9 @@ where
}
}
impl<T, U> Component for ShowMore<T, U>
impl<T> Component for ShowMore<T>
where
T: Component,
U: StringType + Clone,
{
type Msg = CancelInfoConfirmMsg;
@ -83,10 +82,9 @@ where
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
impl<T, U> crate::trace::Trace for ShowMore<T, U>
impl<T> crate::trace::Trace for ShowMore<T>
where
T: crate::trace::Trace + Component,
U: StringType + Clone,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("ShowMore");

@ -127,6 +127,6 @@ where
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("Title");
t.string("text", self.title.as_ref());
t.string("text", self.title.as_ref().into());
}
}

@ -58,6 +58,6 @@ impl Component for WelcomeScreen {
impl crate::trace::Trace for WelcomeScreen {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("WelcomeScreen");
t.string("model_name", "Trezor Safe 3");
t.string("model_name", "Trezor Safe 3".into());
}
}

File diff suppressed because it is too large Load Diff

@ -2,7 +2,9 @@ use heapless::Vec;
use crate::{
error::Error,
micropython::buffer::StrBuffer,
strutil::StringType,
translations::TR,
ui::{
component::{
text::paragraphs::{Paragraph, ParagraphSource, ParagraphVecShort, Paragraphs, VecExt},
@ -18,7 +20,7 @@ const MAX_XPUBS: usize = 16;
pub struct AddressDetails<T> {
qr_code: Frame<Qr, T>,
details: Frame<Paragraphs<ParagraphVecShort<T>>, T>,
details: Frame<Paragraphs<ParagraphVecShort<StrBuffer>>, T>,
xpub_view: Frame<Paragraphs<Paragraph<T>>, T>,
xpubs: Vec<(T, T), MAX_XPUBS>,
xpub_page_count: Vec<u8, MAX_XPUBS>,
@ -34,21 +36,24 @@ where
qr_address: T,
case_sensitive: bool,
details_title: T,
account: Option<T>,
path: Option<T>,
account: Option<StrBuffer>,
path: Option<StrBuffer>,
) -> Result<Self, Error>
where
T: From<&'static str>,
{
let mut para = ParagraphVecShort::new();
if let Some(a) = account {
para.add(Paragraph::new(&theme::TEXT_NORMAL, "Account:".into()));
para.add(Paragraph::new(
&theme::TEXT_NORMAL,
TR::words__account_colon.try_into()?,
));
para.add(Paragraph::new(&theme::TEXT_MONO, a));
}
if let Some(p) = path {
para.add(Paragraph::new(
&theme::TEXT_NORMAL,
"Derivation path:".into(),
TR::address_details__derivation_path.try_into()?,
));
para.add(Paragraph::new(&theme::TEXT_MONO, p));
}

@ -50,8 +50,8 @@ pub struct Confirm<T> {
title: ConfirmTitle<T>,
message: Child<Label<T>>,
alert: Option<Child<Label<T>>>,
left_button: Child<Button<&'static str>>,
right_button: Child<Button<&'static str>>,
left_button: Child<Button<T>>,
right_button: Child<Button<T>>,
info: Option<ConfirmInfo<T>>,
show_info: bool,
}
@ -62,8 +62,8 @@ where
{
pub fn new(
bg_color: Color,
left_button: Button<&'static str>,
right_button: Button<&'static str>,
left_button: Button<T>,
right_button: Button<T>,
title: ConfirmTitle<T>,
message: Label<T>,
) -> Self {

@ -329,10 +329,10 @@ where
t.component("Button");
match &self.content {
ButtonContent::Empty => {}
ButtonContent::Text(text) => t.string("text", text.as_ref()),
ButtonContent::Text(text) => t.string("text", text.as_ref().into()),
ButtonContent::Icon(_) => t.bool("icon", true),
ButtonContent::IconAndText(content) => {
t.string("text", content.text);
t.string("text", content.text.into());
t.bool("icon", true);
}
ButtonContent::IconBlend(_, _, _) => t.bool("icon", true),

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

Loading…
Cancel
Save