diff --git a/ci/build.yml b/ci/build.yml index 8faa98da557..3f2861edf04 100644 --- a/ci/build.yml +++ b/ci/build.yml @@ -307,6 +307,7 @@ core unix frozen debug build: needs: [] variables: PYOPT: "0" + THP: "1" script: - $NIX_SHELL --run "poetry run make -C core build_unix_frozen" artifacts: diff --git a/common/protob/messages-common.proto b/common/protob/messages-common.proto index 500ddf73894..d24fc76b3de 100644 --- a/common/protob/messages-common.proto +++ b/common/protob/messages-common.proto @@ -39,6 +39,8 @@ message Failure { Failure_PinMismatch = 12; Failure_WipeCodeMismatch = 13; Failure_InvalidSession = 14; + Failure_ThpUnallocatedSession=15; + Failure_InvalidProtocol=16; Failure_FirmwareError = 99; } } diff --git a/common/protob/messages-debug.proto b/common/protob/messages-debug.proto index bdac48b0c23..148c01369bf 100644 --- a/common/protob/messages-debug.proto +++ b/common/protob/messages-debug.proto @@ -51,7 +51,7 @@ message DebugLinkDecision { optional uint32 x = 4; // touch X coordinate optional uint32 y = 5; // touch Y coordinate - optional bool wait = 6; // wait for layout change + optional bool wait = 6 [deprecated=true]; // wait for layout change optional uint32 hold_ms = 7; // touch hold duration optional DebugPhysicalButton physical_button = 8; // physical button press } @@ -61,6 +61,7 @@ message DebugLinkDecision { * @end */ message DebugLinkLayout { + option deprecated = true; repeated string tokens = 1; } @@ -89,9 +90,28 @@ message DebugLinkRecordScreen { * @next DebugLinkState */ message DebugLinkGetState { - optional bool wait_word_list = 1; // Trezor T only - wait until mnemonic words are shown - optional bool wait_word_pos = 2; // Trezor T only - wait until reset word position is requested - optional bool wait_layout = 3; // wait until current layout changes + /// Wait behavior of the call. + enum DebugWaitType { + /// Respond immediately. If no layout is currently displayed, the layout + /// response will be empty. + IMMEDIATE = 0; + /// Wait for next layout. If a layout is displayed, waits for it to change. + /// If no layout is displayed, waits for one to come up. + NEXT_LAYOUT = 1; + /// Return current layout. If no layout is currently displayed, waits for + /// one to come up. + CURRENT_LAYOUT = 2; + } + + // Trezor T < 2.6.0 only - wait until mnemonic words are shown + optional bool wait_word_list = 1 [deprecated=true]; + // Trezor T < 2.6.0 only - wait until reset word position is requested + optional bool wait_word_pos = 2 [deprecated=true]; + // trezor-core only - wait until current layout changes + // changed in 2.6.4: multiple wait types instead of true/false. + optional DebugWaitType wait_layout = 3 [default=IMMEDIATE]; + + optional bytes thp_channel_id=4; // THP only - used to get information from particular channel } /** @@ -112,6 +132,9 @@ message DebugLinkState { optional uint32 reset_word_pos = 11; // index of mnemonic word the device is expecting during ResetDevice workflow optional management.BackupType mnemonic_type = 12; // current mnemonic type (BIP-39/SLIP-39) repeated string tokens = 13; // current layout represented as a list of string tokens + optional uint32 thp_pairing_code_entry_code = 14; + optional bytes thp_pairing_code_qr_code = 15; + optional bytes thp_pairing_code_nfc_unidirectional = 16; } /** @@ -192,6 +215,7 @@ message DebugLinkEraseSdCard { * @next Success */ message DebugLinkWatchLayout { + option deprecated = true; optional bool watch = 1; // if true, start watching layout. // if false, stop. } @@ -203,6 +227,7 @@ message DebugLinkWatchLayout { * @next Success */ message DebugLinkResetDebugEvents { + option deprecated = true; } diff --git a/common/protob/messages-thp.proto b/common/protob/messages-thp.proto index 743cf3a1eee..579a06c5382 100644 --- a/common/protob/messages-thp.proto +++ b/common/protob/messages-thp.proto @@ -9,6 +9,189 @@ option (include_in_bitcoin_only) = true; import "messages.proto"; +/** + * Numeric identifiers of pairing methods. + * @embed + */ +enum ThpPairingMethod { + NoMethod = 1; // Trust without MITM protection. + CodeEntry = 2; // User types code diplayed on Trezor into the host application. + QrCode = 3; // User scans code displayed on Trezor into host application. + NFC_Unidirectional = 4; // Trezor transmits an authentication key to the host device via NFC. +} + +/** + * @embed + */ +message ThpDeviceProperties { + optional string internal_model = 1; // Internal model name e.g. "T2B1". + optional uint32 model_variant = 2; // Encodes the device properties such as color. + optional bool bootloader_mode = 3; // Indicates whether the device is in bootloader or firmware mode. + optional uint32 protocol_version = 4; // The communication protocol version supported by the firmware. + repeated ThpPairingMethod pairing_methods = 5; // The pairing methods supported by the Trezor. +} + +/** + * @embed + */ +message ThpHandshakeCompletionReqNoisePayload { + optional bytes host_pairing_credential = 1; // Host's pairing credential + repeated ThpPairingMethod pairing_methods = 2; // The pairing methods chosen by the host +} + +/** + * Request: Ask device for a new session with given passphrase. + * @start + * @next ThpNewSession + */ +message ThpCreateNewSession{ + optional string passphrase = 1; + optional bool on_device = 2; // User wants to enter passphrase on the device + optional bool derive_cardano = 3; // If True, Cardano keys will be derived. Ignored with BTC-only +} + +/** + * Response: Contains session_id of the newly created session. + * @end + */ +message ThpNewSession{ + optional uint32 new_session_id = 1; +} + +/** + * Request: Start pairing process. + * @start + * @next ThpCodeEntryCommitment + * @next ThpPairingPreparationsFinished + */ +message ThpStartPairingRequest{ + optional string host_name = 1; // Human-readable host name +} + +/** + * Response: Pairing is ready for user input / OOB communication. + * @next ThpCodeEntryCpace + * @next ThpQrCodeTag + * @next ThpNfcUnidirectionalTag + */ + message ThpPairingPreparationsFinished{ +} + +/** + * Response: If Code Entry is an allowed pairing option, Trezor responds with a commitment. + * @next ThpCodeEntryChallenge + */ +message ThpCodeEntryCommitment { + optional bytes commitment = 1; // SHA-256 of Trezor's random 32-byte secret +} + +/** + * Response: Host responds to Trezor's Code Entry commitment with a challenge. + * @next ThpPairingPreparationsFinished + */ +message ThpCodeEntryChallenge { + optional bytes challenge = 1; // host's random 32-byte challenge +} + +/** + * Request: User selected Code Entry option in Host. Host starts CPACE protocol with Trezor. + * @next ThpCodeEntryCpaceTrezor + */ +message ThpCodeEntryCpaceHost { + optional bytes cpace_host_public_key = 1; // Host's ephemeral CPace public key +} + +/** + * Response: Trezor continues with the CPACE protocol. + * @next ThpCodeEntryTag + */ +message ThpCodeEntryCpaceTrezor { + optional bytes cpace_trezor_public_key = 1; // Trezor's ephemeral CPace public key +} + +/** + * Response: Host continues with the CPACE protocol. + * @next ThpCodeEntrySecret + */ +message ThpCodeEntryTag { + optional bytes tag = 2; // SHA-256 of shared secret +} + +/** + * Response: Trezor finishes the CPACE protocol. + * @next ThpCredentialRequest + * @next ThpEndRequest + */ +message ThpCodeEntrySecret { + optional bytes secret = 1; // Trezor's secret +} + +/** + * Request: User selected QR Code pairing option. Host sends a QR Tag. + * @next ThpQrCodeSecret + */ +message ThpQrCodeTag { + optional bytes tag = 1; // SHA-256 of shared secret +} + +/** + * Response: Trezor sends the QR secret. + * @next ThpCredentialRequest + * @next ThpEndRequest + */ +message ThpQrCodeSecret { + optional bytes secret = 1; // Trezor's secret +} + +/** + * Request: User selected Unidirectional NFC pairing option. Host sends an Unidirectional NFC Tag. + * @next ThpNfcUnidirectionalSecret + */ +message ThpNfcUnidirectionalTag { + optional bytes tag = 1; // SHA-256 of shared secret +} + +/** + * Response: Trezor sends the Unidirectioal NFC secret. + * @next ThpCredentialRequest + * @next ThpEndRequest + */ +message ThpNfcUnidirectionalSecret { + optional bytes secret = 1; // Trezor's secret +} + +/** + * Request: Host requests issuance of a new pairing credential. + * @start + * @next ThpCredentialResponse + */ +message ThpCredentialRequest { + optional bytes host_static_pubkey = 1; // Host's static public key used in the handshake. +} + +/** + * Response: Trezor issues a new pairing credential. + * @next ThpCredentialRequest + * @next ThpEndRequest + */ +message ThpCredentialResponse { + optional bytes trezor_static_pubkey = 1; // Trezor's static public key used in the handshake. + optional bytes credential = 2; // The pairing credential issued by the Trezor to the host. +} + +/** + * Request: Host requests transition to the encrypted traffic phase. + * @start + * @next ThpEndResponse + */ +message ThpEndRequest {} + +/** + * Response: Trezor approves transition to the encrypted traffic phase + * @end + */ +message ThpEndResponse {} + /** * Only for internal use. * @embed diff --git a/common/protob/messages.proto b/common/protob/messages.proto index 8156bc119cb..55bfbeaadd7 100644 --- a/common/protob/messages.proto +++ b/common/protob/messages.proto @@ -42,6 +42,10 @@ extend google.protobuf.EnumValueOptions { optional bool wire_tiny = 50006; // message is handled by Trezor when the USB stack is in tiny mode optional bool wire_bootloader = 50007; // message is only handled by Trezor Bootloader optional bool wire_no_fsm = 50008; // message is not handled by Trezor unless the USB stack is in tiny mode + optional bool channel_in = 50009; + optional bool channel_out = 50010; + optional bool pairing_in = 50011; + optional bool pairing_out = 50012; optional bool bitcoin_only = 60000; // enum value is available on BITCOIN_ONLY build // (messages not marked bitcoin_only will be EXCLUDED) @@ -376,4 +380,26 @@ enum MessageType { MessageType_SolanaAddress = 903 [(wire_out) = true]; MessageType_SolanaSignTx = 904 [(wire_in) = true]; MessageType_SolanaTxSignature = 905 [(wire_out) = true]; + + // THP + MessageType_ThpCreateNewSession = 1000[(bitcoin_only)=true, (channel_in) = true]; + MessageType_ThpNewSession = 1001[(bitcoin_only)=true, (channel_out) = true]; + MessageType_ThpStartPairingRequest = 1008 [(bitcoin_only) = true, (pairing_in) = true]; + MessageType_ThpPairingPreparationsFinished = 1009 [(bitcoin_only) = true, (pairing_out) = true]; + MessageType_ThpCredentialRequest = 1010 [(bitcoin_only) = true, (pairing_in) = true]; + MessageType_ThpCredentialResponse = 1011 [(bitcoin_only) = true, (pairing_out) = true]; + MessageType_ThpEndRequest = 1012 [(bitcoin_only) = true, (pairing_in) = true]; + MessageType_ThpEndResponse = 1013[(bitcoin_only) = true, (pairing_out) = true]; + MessageType_ThpCodeEntryCommitment = 1016[(bitcoin_only)=true, (pairing_out) = true]; + MessageType_ThpCodeEntryChallenge = 1017[(bitcoin_only)=true, (pairing_in) = true]; + MessageType_ThpCodeEntryCpaceHost = 1018[(bitcoin_only)=true, (pairing_in) = true]; + MessageType_ThpCodeEntryCpaceTrezor = 1019[(bitcoin_only)=true, (pairing_out) = true]; + MessageType_ThpCodeEntryTag = 1020[(bitcoin_only)=true, (pairing_in) = true]; + MessageType_ThpCodeEntrySecret = 1021[(bitcoin_only)=true, (pairing_out) = true]; + MessageType_ThpQrCodeTag = 1024[(bitcoin_only)=true, (pairing_in) = true]; + MessageType_ThpQrCodeSecret = 1025[(bitcoin_only)=true, (pairing_out) = true]; + MessageType_ThpNfcUnidirectionalTag = 1032[(bitcoin_only)=true, (pairing_in) = true]; + MessageType_ThpNfcUnidirectionalSecret = 1033[(bitcoin_only)=true, (pairing_in) = true]; } + + diff --git a/common/protob/pb2py b/common/protob/pb2py index 5eddadd42a2..c1b94b1c9c8 100755 --- a/common/protob/pb2py +++ b/common/protob/pb2py @@ -558,6 +558,8 @@ class RustBlobRenderer: enums = [] cursor = 0 for enum in sorted(self.descriptor.enums, key=lambda e: e.name): + if enum.name == "MessageType": + continue self.enum_map[enum.name] = cursor enum_blob = ENUM_ENTRY.build(sorted(v.number for v in enum.value)) enums.append(enum_blob) diff --git a/core/.changelog.d/2299.changed b/core/.changelog.d/2299.changed new file mode 100644 index 00000000000..72769775314 --- /dev/null +++ b/core/.changelog.d/2299.changed @@ -0,0 +1 @@ +Improve UI synchronization, ordering, and responsiveness (Global Layout project). diff --git a/core/.changelog.d/3633.changed b/core/.changelog.d/3633.changed new file mode 100644 index 00000000000..dcc32f9a268 --- /dev/null +++ b/core/.changelog.d/3633.changed @@ -0,0 +1 @@ +Improved device responsiveness by removing unnecessary screen refreshes. diff --git a/core/Makefile b/core/Makefile index 1755fd6745d..b7f13242041 100644 --- a/core/Makefile +++ b/core/Makefile @@ -127,6 +127,7 @@ TREZOR_FIDO2_UDP_PORT = 21326 RUST_TARGET=$(shell rustc -vV | sed -n 's/host: //p') MULTICORE ?= "auto" +RANDOM=$(shell python -c 'import random; print(random.randint(0, 1000000))') ## help commands: @@ -162,7 +163,7 @@ test_emu: ## run selected device tests from python-trezor 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=$(RANDOM) \ --lang=$(TEST_LANG) test_emu_monero: ## run selected monero device tests from monero-agent @@ -198,7 +199,7 @@ test_emu_ui: ## run ui integration tests 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=$(RANDOM) \ --lang=$(TEST_LANG) test_emu_ui_record: ## record and hash screens for ui integration tests @@ -297,14 +298,20 @@ build_unix: templates ## build unix port $(SCONS) CFLAGS="$(CFLAGS)" $(UNIX_BUILD_DIR)/trezor-emu-core $(UNIX_PORT_OPTS) \ TREZOR_MODEL="$(TREZOR_MODEL)" CMAKELISTS="$(CMAKELISTS)" THP="$(THP)" \ PYOPT="0" BITCOIN_ONLY="$(BITCOIN_ONLY)" TREZOR_EMULATOR_ASAN="$(ADDRESS_SANITIZER)" \ - NEW_RENDERING="$(NEW_RENDERING)" + NEW_RENDERING="$(NEW_RENDERING)" TREZOR_MEMPERF="$(TREZOR_MEMPERF)" build_unix_frozen: templates build_cross ## build unix port with frozen modules $(SCONS) CFLAGS="$(CFLAGS)" $(UNIX_BUILD_DIR)/trezor-emu-core $(UNIX_PORT_OPTS) \ - TREZOR_MODEL="$(TREZOR_MODEL)" CMAKELISTS="$(CMAKELISTS)" \ + TREZOR_MODEL="$(TREZOR_MODEL)" CMAKELISTS="$(CMAKELISTS)" THP="$(THP)"\ PYOPT="$(PYOPT)" BITCOIN_ONLY="$(BITCOIN_ONLY)" TREZOR_EMULATOR_ASAN="$(ADDRESS_SANITIZER)" \ TREZOR_MEMPERF="$(TREZOR_MEMPERF)" TREZOR_EMULATOR_FROZEN=1 NEW_RENDERING="$(NEW_RENDERING)" +build_unix_frozen_debug: templates build_cross ## build unix port with frozen modules and DEBUG (PYOPT="0") + $(SCONS) CFLAGS="$(CFLAGS)" $(UNIX_BUILD_DIR)/trezor-emu-core $(UNIX_PORT_OPTS) \ + TREZOR_MODEL="$(TREZOR_MODEL)" CMAKELISTS="$(CMAKELISTS)" THP="$(THP)"\ + PYOPT="0" BITCOIN_ONLY="$(BITCOIN_ONLY)" TREZOR_EMULATOR_ASAN="$(ADDRESS_SANITIZER)" \ + TREZOR_MEMPERF="$(TREZOR_MEMPERF)" TREZOR_EMULATOR_FROZEN=1 + build_unix_debug: templates ## build unix port $(SCONS) --max-drift=1 CFLAGS="$(CFLAGS)" $(UNIX_BUILD_DIR)/trezor-emu-core $(UNIX_PORT_OPTS) \ TREZOR_MODEL="$(TREZOR_MODEL)" CMAKELISTS="$(CMAKELISTS)" \ diff --git a/core/SConscript.firmware b/core/SConscript.firmware index 04da78afac7..3f357ad3dda 100644 --- a/core/SConscript.firmware +++ b/core/SConscript.firmware @@ -24,6 +24,19 @@ FEATURE_FLAGS = { "AES_GCM": False, } + +if THP: + FEATURE_FLAGS = { + "RDI": True, + "SECP256K1_ZKP": True, # required for trezor.crypto.curve.bip340 (BIP340/Taproot) + "AES_GCM": True, # Required for THP encryption + } +else: + FEATURE_FLAGS = { + "RDI": True, + "SECP256K1_ZKP": True, # required for trezor.crypto.curve.bip340 (BIP340/Taproot) + "AES_GCM": False, + } FEATURES_WANTED = ["input", "sbu", "sd_card", "rgb_led", "dma2d", "consumption_mask", "usb" ,"optiga", "haptic"] if DISABLE_OPTIGA and PYOPT == '0': FEATURES_WANTED.remove("optiga") @@ -567,6 +580,8 @@ if FROZEN: ] if not EVERYTHING else [] )) + if THP: + SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/wire/thp/*.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/wire/*.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'storage/*.py', @@ -616,6 +631,8 @@ if FROZEN: SOURCE_PY_DIR + 'apps/bitcoin/sign_tx/zcash_v4.py', ]) ) + if THP: + SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/thp/*.py')) if EVERYTHING: SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/binance/*.py')) @@ -677,6 +694,8 @@ if FROZEN: bitcoin_only=BITCOIN_ONLY, backlight='backlight' in FEATURES_AVAILABLE, optiga='optiga' in FEATURES_AVAILABLE, + use_button='button' in FEATURES_AVAILABLE, + use_touch='touch' in FEATURES_AVAILABLE, ui_layout=ui.get_ui_layout(TREZOR_MODEL), thp=THP, ) @@ -694,7 +713,7 @@ if FROZEN: source_files = SOURCE_MOD + SOURCE_MOD_CRYPTO + SOURCE_FIRMWARE + SOURCE_MICROPYTHON + SOURCE_MICROPYTHON_SPEED + SOURCE_HAL obj_program = [] obj_program.extend(env.Object(source=SOURCE_MOD)) -obj_program.extend(env.Object(source=SOURCE_MOD_CRYPTO, CCFLAGS='$CCFLAGS -ftrivial-auto-var-init=zero')) +obj_program.extend(env.Object(source=SOURCE_MOD_CRYPTO)) if FEATURE_FLAGS["SECP256K1_ZKP"]: obj_program.extend(env.Object(source=SOURCE_MOD_SECP256K1_ZKP, CCFLAGS='$CCFLAGS -Wno-unused-function')) source_files.extend(SOURCE_MOD_SECP256K1_ZKP) diff --git a/core/SConscript.unix b/core/SConscript.unix index aa46f36af61..8a958b5b329 100644 --- a/core/SConscript.unix +++ b/core/SConscript.unix @@ -440,18 +440,6 @@ env = Environment(ENV=os.environ, CFLAGS='%s -DCONFIDENTIAL= -DPYOPT=%s -DBITCOI FEATURES_AVAILABLE = models.configure_board(TREZOR_MODEL, HW_REVISION, FEATURES_WANTED, env, CPPDEFINES_HAL, SOURCE_UNIX, PATH_HAL) - -if 'sd_card' in FEATURES_AVAILABLE: - SDCARD = True -else: - SDCARD = False - -if 'optiga' in FEATURES_AVAILABLE: - OPTIGA = True -else: - OPTIGA = False - - env.Tool('micropython') env.Replace( @@ -494,7 +482,7 @@ if ARGUMENTS.get('TREZOR_EMULATOR_DEBUGGABLE', '0') == '1': if ARGUMENTS.get('TREZOR_MEMPERF', '0') == '1': CPPDEFINES_MOD += [ - ('MICROPY_TREZOR_MEMPERF', '\(1\)') + ('MICROPY_TREZOR_MEMPERF', '1') ] env.Replace( @@ -625,7 +613,7 @@ if FROZEN: SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/*.py', exclude=[ SOURCE_PY_DIR + 'trezor/sdcard.py', - ] if not SDCARD else [] + ] if 'sd_card' not in FEATURES_AVAILABLE else [] )) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/crypto/*.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/*.py')) @@ -645,12 +633,14 @@ if FROZEN: ] if not EVERYTHING else [] )) + if THP: + SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/wire/thp/*.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/wire/*.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'storage/*.py', exclude=[ SOURCE_PY_DIR + 'storage/sd_salt.py', - ] if not SDCARD else [] + ] if 'sd_card' not in FEATURES_AVAILABLE else [] )) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/messages/__init__.py')) @@ -675,16 +665,16 @@ if FROZEN: SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/common/*.py', exclude=[ SOURCE_PY_DIR + 'apps/common/sdcard.py', - ] if not SDCARD else [] + ] if "sd_card" not in FEATURES_AVAILABLE else [] )) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/debug/*.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/homescreen/*.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/management/*.py', exclude=[ SOURCE_PY_DIR + 'apps/management/sd_protect.py', - ] if not SDCARD else [] + [ + ] if "sd_card" not in FEATURES_AVAILABLE else [] + [ SOURCE_PY_DIR + 'apps/management/authenticate_device.py', - ] if not OPTIGA else []) + ] if "optiga" not in FEATURES_AVAILABLE else []) ) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/management/*/*.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/misc/*.py')) @@ -756,7 +746,9 @@ if FROZEN: source_dir=SOURCE_PY_DIR, bitcoin_only=BITCOIN_ONLY, backlight='backlight' in FEATURES_AVAILABLE, - optiga=OPTIGA, + optiga='optiga' in FEATURES_AVAILABLE, + use_button='button' in FEATURES_AVAILABLE, + use_touch='touch' in FEATURES_AVAILABLE, ui_layout=ui.get_ui_layout(TREZOR_MODEL), thp=THP, ) diff --git a/core/embed/extmod/modtrezorcrypto/modtrezorcrypto-aesgcm.h b/core/embed/extmod/modtrezorcrypto/modtrezorcrypto-aesgcm.h index b7edf784b84..80dfaa809ec 100644 --- a/core/embed/extmod/modtrezorcrypto/modtrezorcrypto-aesgcm.h +++ b/core/embed/extmod/modtrezorcrypto/modtrezorcrypto-aesgcm.h @@ -111,9 +111,9 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_trezorcrypto_AesGcm_encrypt_obj, mod_trezorcrypto_AesGcm_encrypt); /// def encrypt_in_place(self, data: bytearray | memoryview) -> int: -/// """ -/// Encrypt data chunk in place. Returns the length of the encrypted data. -/// """ +/// """ +/// Encrypt data chunk in place. Returns the length of the encrypted data. +/// """ STATIC mp_obj_t mod_trezorcrypto_AesGcm_encrypt_in_place(mp_obj_t self, mp_obj_t data) { mp_obj_AesGcm_t *o = MP_OBJ_TO_PTR(self); @@ -158,9 +158,9 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_trezorcrypto_AesGcm_decrypt_obj, mod_trezorcrypto_AesGcm_decrypt); /// def decrypt_in_place(self, data: bytearray | memoryview) -> int: -/// """ -/// Decrypt data chunk in place. Returns the length of the decrypted data. -/// """ +/// """ +/// Decrypt data chunk in place. Returns the length of the decrypted data. +/// """ STATIC mp_obj_t mod_trezorcrypto_AesGcm_decrypt_in_place(mp_obj_t self, mp_obj_t data) { mp_obj_AesGcm_t *o = MP_OBJ_TO_PTR(self); diff --git a/core/embed/extmod/modtrezorutils/modtrezorutils.c b/core/embed/extmod/modtrezorutils/modtrezorutils.c index 21444bc5a7c..4557c554c58 100644 --- a/core/embed/extmod/modtrezorutils/modtrezorutils.c +++ b/core/embed/extmod/modtrezorutils/modtrezorutils.c @@ -389,6 +389,10 @@ STATIC mp_obj_tuple_t mod_trezorutils_version_obj = { /// """Whether the hardware supports haptic feedback.""" /// USE_OPTIGA: bool /// """Whether the hardware supports Optiga secure element.""" +/// USE_TOUCH: bool +/// """Whether the hardware supports touch screen.""" +/// USE_BUTTON: bool +/// """Whether the hardware supports two-button input.""" /// MODEL: str /// """Model name.""" /// MODEL_FULL_NAME: str @@ -406,7 +410,7 @@ STATIC mp_obj_tuple_t mod_trezorutils_version_obj = { /// UI_LAYOUT: str /// """UI layout identifier ("tt" for model T, "tr" for models One and R).""" /// USE_THP: bool -/// """Whether the firmware supports Trezor-Host Protocol (version 3).""" +/// """Whether the firmware supports the Trezor-Host Protocol.""" STATIC const mp_rom_map_elem_t mp_module_trezorutils_globals_table[] = { {MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_trezorutils)}, @@ -454,6 +458,16 @@ STATIC const mp_rom_map_elem_t mp_module_trezorutils_globals_table[] = { {MP_ROM_QSTR(MP_QSTR_USE_OPTIGA), mp_const_true}, #else {MP_ROM_QSTR(MP_QSTR_USE_OPTIGA), mp_const_false}, +#endif +#ifdef USE_TOUCH + {MP_ROM_QSTR(MP_QSTR_USE_TOUCH), mp_const_true}, +#else + {MP_ROM_QSTR(MP_QSTR_USE_TOUCH), mp_const_false}, +#endif +#ifdef USE_BUTTON + {MP_ROM_QSTR(MP_QSTR_USE_BUTTON), mp_const_true}, +#else + {MP_ROM_QSTR(MP_QSTR_USE_BUTTON), mp_const_false}, #endif {MP_ROM_QSTR(MP_QSTR_MODEL), MP_ROM_PTR(&mod_trezorutils_model_name_obj)}, {MP_ROM_QSTR(MP_QSTR_MODEL_FULL_NAME), diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h index 83721d3768b..1a832dfd052 100644 --- a/core/embed/rust/librust_qstr.h +++ b/core/embed/rust/librust_qstr.h @@ -6,15 +6,18 @@ static void _librust_qstrs(void) { MP_QSTR_; + MP_QSTR_ATTACHED; MP_QSTR_AttachType; MP_QSTR_BacklightLevels; MP_QSTR_CANCELLED; MP_QSTR_CONFIRMED; MP_QSTR_DIM; + MP_QSTR_DONE; MP_QSTR_INFO; MP_QSTR_INITIAL; MP_QSTR_LOW; MP_QSTR_LayoutObj; + MP_QSTR_LayoutState; MP_QSTR_MAX; MP_QSTR_MESSAGE_NAME; MP_QSTR_MESSAGE_WIRE_TYPE; @@ -28,6 +31,7 @@ static void _librust_qstrs(void) { MP_QSTR_SWIPE_RIGHT; MP_QSTR_SWIPE_UP; MP_QSTR_TR; + MP_QSTR_TRANSITIONING; MP_QSTR_TranslationsHeader; MP_QSTR___del__; MP_QSTR___dict__; @@ -571,6 +575,7 @@ static void _librust_qstrs(void) { MP_QSTR_reset__wrong_word_selected; MP_QSTR_reset__you_need_one_share; MP_QSTR_reset__your_backup_is_done; + MP_QSTR_return_value; MP_QSTR_reverse; MP_QSTR_rotation__change_template; MP_QSTR_rotation__east; @@ -642,6 +647,7 @@ static void _librust_qstrs(void) { MP_QSTR_show_homescreen; MP_QSTR_show_info; MP_QSTR_show_info_with_cancel; + MP_QSTR_show_instructions; MP_QSTR_show_lockscreen; MP_QSTR_show_mismatch; MP_QSTR_show_passphrase; diff --git a/core/embed/rust/src/micropython/macros.rs b/core/embed/rust/src/micropython/macros.rs index c058bcb429f..750d12dcaf6 100644 --- a/core/embed/rust/src/micropython/macros.rs +++ b/core/embed/rust/src/micropython/macros.rs @@ -79,9 +79,9 @@ macro_rules! obj_fn_kw { /// Construct fixed static const `Map` from `key` => `val` pairs. macro_rules! obj_map { ($($key:expr => $val:expr),*) => ({ - Map::from_fixed_static(&[ + $crate::micropython::map::Map::from_fixed_static(&[ $( - Map::at($key, $val), + $crate::micropython::map::Map::at($key, $val), )* ]) }); @@ -110,6 +110,7 @@ macro_rules! obj_dict { /// Compose a `Type` object definition. macro_rules! obj_type { (name: $name:expr, + $(base: $base:expr,)? $(locals: $locals:expr,)? $(make_new_fn: $make_new_fn:path,)? $(attr_fn: $attr_fn:path,)? @@ -121,6 +122,11 @@ macro_rules! obj_type { let name = $name.to_u16(); + #[allow(unused_mut)] + #[allow(unused_assignments)] + let mut base_type: &'static ffi::mp_obj_type_t = &ffi::mp_type_type; + $(base_type = &$base;)? + #[allow(unused_mut)] #[allow(unused_assignments)] let mut attr: ffi::mp_attr_fun_t = None; @@ -146,7 +152,7 @@ macro_rules! obj_type { ffi::mp_obj_type_t { base: ffi::mp_obj_base_t { - type_: &ffi::mp_type_type, + type_: base_type, }, flags: 0, name, diff --git a/core/embed/rust/src/protobuf/obj.rs b/core/embed/rust/src/protobuf/obj.rs index 7b4c69033cb..fc3a1f3a1dd 100644 --- a/core/embed/rust/src/protobuf/obj.rs +++ b/core/embed/rust/src/protobuf/obj.rs @@ -356,7 +356,7 @@ pub static mp_module_trezorproto: Module = obj_module! { /// """Calculate length of encoding of the specified message.""" Qstr::MP_QSTR_encoded_length => obj_fn_1!(protobuf_len).as_obj(), - /// def encode(buffer: bytearray, msg: MessageType) -> int: + /// def encode(buffer: bytearray | memoryview, msg: MessageType) -> int: /// """Encode the message into the specified buffer. Return length of /// encoding.""" Qstr::MP_QSTR_encode => obj_fn_2!(protobuf_encode).as_obj() diff --git a/core/embed/rust/src/ui/button_request.rs b/core/embed/rust/src/ui/button_request.rs index 58fe5ff178c..06dfdd77ff5 100644 --- a/core/embed/rust/src/ui/button_request.rs +++ b/core/embed/rust/src/ui/button_request.rs @@ -3,7 +3,7 @@ use num_traits::FromPrimitive; // ButtonRequestType from messages-common.proto // Eventually this should be generated -#[derive(Clone, Copy, FromPrimitive)] +#[derive(Clone, Copy, FromPrimitive, PartialEq, Eq)] #[repr(u16)] pub enum ButtonRequestCode { Other = 1, @@ -41,7 +41,7 @@ impl ButtonRequestCode { } } -#[derive(Clone)] +#[derive(Copy, Clone, PartialEq, Eq)] pub struct ButtonRequest { pub code: ButtonRequestCode, pub name: TString<'static>, diff --git a/core/embed/rust/src/ui/component/base.rs b/core/embed/rust/src/ui/component/base.rs index b74d85c9389..10faf12e5d5 100644 --- a/core/embed/rust/src/ui/component/base.rs +++ b/core/embed/rust/src/ui/component/base.rs @@ -26,6 +26,7 @@ use super::Paginate; /// Type used by components that do not return any messages. /// /// Alternative to the yet-unstable `!`-type. +#[cfg_attr(feature = "debug", derive(ufmt::derive::uDebug))] pub enum Never {} /// User interface is composed of components that can react to `Event`s through @@ -142,7 +143,7 @@ where // Handle the internal invalidation event here, so components don't have to. We // still pass it inside, so the event propagates correctly to all components in // the sub-tree. - if let Event::RequestPaint = event { + if matches!(event, Event::RequestPaint | Event::Attach(_)) { ctx.request_paint(); } c.event(ctx, event) @@ -370,8 +371,9 @@ pub enum Event { Timer(TimerToken), /// Advance progress bar. Progress screens only. Progress(u16, TString<'static>), - /// Component has been attached to component tree. This event is sent once - /// before any other events. + /// Component has been attached to component tree, all children should + /// prepare for painting and/or start their timers. + /// This event is sent once before any other events. Attach(AttachType), /// Internally-handled event to inform all `Child` wrappers in a sub-tree to /// get scheduled for painting. @@ -401,6 +403,11 @@ pub struct TimerToken(u32); impl TimerToken { /// Value of an invalid (or missing) token. pub const INVALID: TimerToken = TimerToken(0); + /// Reserved value of the animation frame timer. + pub const ANIM_FRAME: TimerToken = TimerToken(1); + + /// Starting token value + const STARTING_TOKEN: u32 = 2; pub const fn from_raw(raw: u32) -> Self { Self(raw) @@ -409,11 +416,77 @@ impl TimerToken { pub const fn into_raw(self) -> u32 { self.0 } + + pub fn next_token() -> Self { + static mut NEXT_TOKEN: TimerToken = Self(TimerToken::STARTING_TOKEN); + + // SAFETY: we are in single-threaded environment + let token = unsafe { NEXT_TOKEN }; + let next = { + if token.0 == u32::MAX { + TimerToken(Self::STARTING_TOKEN) + } else { + TimerToken(token.0 + 1) + } + }; + // SAFETY: we are in single-threaded environment + unsafe { NEXT_TOKEN = next }; + token + } +} + +#[cfg_attr(feature = "debug", derive(ufmt::derive::uDebug))] +pub struct Timer { + token: TimerToken, + running: bool, +} + +impl Timer { + /// Create a new timer. + pub const fn new() -> Self { + Self { + token: TimerToken::INVALID, + running: false, + } + } + + /// Start this timer for a given duration. + /// + /// Requests the internal timer token to be scheduled to `duration` from + /// now. If the timer was already running, its token is rescheduled. + pub fn start(&mut self, ctx: &mut EventCtx, duration: Duration) { + if self.token == TimerToken::INVALID { + self.token = TimerToken::next_token(); + } + self.running = true; + ctx.register_timer(self.token, duration); + } + + /// Stop the timer. + /// + /// Does not affect scheduling, only clears the internal timer token. This + /// means that _some_ scheduled task might keep running, but this timer + /// will not trigger when that task expires. + pub fn stop(&mut self) { + self.running = false; + } + + /// Check if the timer has expired. + /// + /// Returns `true` if the given event is a timer event and the token matches + /// the internal token of this timer. + pub fn expire(&mut self, event: Event) -> bool { + if self.running && event == Event::Timer(self.token) { + self.running = false; + true + } else { + false + } + } } pub struct EventCtx { timers: Vec<(TimerToken, Duration), { Self::MAX_TIMERS }>, - next_token: u32, place_requested: bool, paint_requested: bool, anim_frame_scheduled: bool, @@ -432,20 +505,14 @@ impl EventCtx { /// How long into the future we should schedule the animation frame timer. const ANIM_FRAME_DURATION: Duration = Duration::from_millis(1); - // 0 == `TimerToken::INVALID`, - // 1 == `Self::ANIM_FRAME_TIMER`. - const STARTING_TIMER_TOKEN: u32 = 2; - /// Maximum amount of timers requested in one event tick. const MAX_TIMERS: usize = 4; pub fn new() -> Self { Self { timers: Vec::new(), - next_token: Self::STARTING_TIMER_TOKEN, - place_requested: true, // We need to perform a place pass in the beginning. - paint_requested: false, /* We also need to paint, but this is supplemented by - * `Child::marked_for_paint` being true. */ + place_requested: false, + paint_requested: false, anim_frame_scheduled: false, page_count: None, button_request: None, @@ -476,13 +543,6 @@ impl EventCtx { self.paint_requested = true; } - /// Request a timer event to be delivered after `duration` elapses. - pub fn request_timer(&mut self, duration: Duration) -> TimerToken { - let token = self.next_timer_token(); - self.register_timer(token, duration); - token - } - /// Request an animation frame timer to fire as soon as possible. pub fn request_anim_frame(&mut self) { if !self.anim_frame_scheduled { @@ -491,6 +551,10 @@ impl EventCtx { } } + pub fn is_anim_frame(event: Event) -> bool { + matches!(event, Event::Timer(token) if token == Self::ANIM_FRAME_TIMER) + } + pub fn request_repaint_root(&mut self) { self.root_repaint_requested = true; } @@ -518,8 +582,7 @@ impl EventCtx { } pub fn send_button_request(&mut self, code: ButtonRequestCode, name: TString<'static>) { - #[cfg(feature = "ui_debug")] - assert!(self.button_request.is_none()); + debug_assert!(self.button_request.is_none()); self.button_request = Some(ButtonRequest::new(code, name)); } @@ -548,17 +611,9 @@ impl EventCtx { } pub fn clear(&mut self) { - self.place_requested = false; - self.paint_requested = false; - self.anim_frame_scheduled = false; - self.page_count = None; - #[cfg(feature = "ui_debug")] - assert!(self.button_request.is_none()); - self.button_request = None; - self.root_repaint_requested = false; - self.swipe_disable_req = false; - self.swipe_enable_req = false; - self.transition_out = None; + debug_assert!(self.button_request.is_none()); + // replace self with a new instance, keeping only the fields we care about + *self = Self::new(); } fn register_timer(&mut self, token: TimerToken, duration: Duration) { @@ -570,18 +625,6 @@ impl EventCtx { } } - fn next_timer_token(&mut self) -> TimerToken { - let token = TimerToken(self.next_token); - // We start again from the beginning if the token counter overflows. This would - // probably happen in case of a bug and a long-running session. Let's risk the - // collisions in such case. - self.next_token = self - .next_token - .checked_add(1) - .unwrap_or(Self::STARTING_TIMER_TOKEN); - token - } - pub fn set_transition_out(&mut self, attach_type: AttachType) { self.transition_out = Some(attach_type); } diff --git a/core/embed/rust/src/ui/component/button_request.rs b/core/embed/rust/src/ui/component/button_request.rs index 273f9858ede..529f2b3e0f0 100644 --- a/core/embed/rust/src/ui/component/button_request.rs +++ b/core/embed/rust/src/ui/component/button_request.rs @@ -53,7 +53,7 @@ impl Component for SendButtonRequest { } } SendButtonRequestPolicy::OnAttachAlways => { - if let Some(br) = self.button_request.clone() { + if let Some(br) = self.button_request { ctx.send_button_request(br.code, br.name); } } diff --git a/core/embed/rust/src/ui/component/marquee.rs b/core/embed/rust/src/ui/component/marquee.rs index d6a91386c89..fff8ca83dec 100644 --- a/core/embed/rust/src/ui/component/marquee.rs +++ b/core/embed/rust/src/ui/component/marquee.rs @@ -3,7 +3,7 @@ use crate::{ time::{Duration, Instant}, ui::{ animation::Animation, - component::{Component, Event, EventCtx, Never, TimerToken}, + component::{Component, Event, EventCtx, Never, Timer}, display::{self, Color, Font}, geometry::{Offset, Rect}, shape::{self, Renderer}, @@ -24,7 +24,7 @@ enum State { pub struct Marquee { area: Rect, - pause_token: Option, + pause_timer: Timer, min_offset: i16, max_offset: i16, state: State, @@ -40,7 +40,7 @@ impl Marquee { pub fn new(text: TString<'static>, font: Font, fg: Color, bg: Color) -> Self { Self { area: Rect::zero(), - pause_token: None, + pause_timer: Timer::new(), min_offset: 0, max_offset: 0, state: State::Initial, @@ -154,53 +154,50 @@ impl Component for Marquee { let now = Instant::now(); - if let Event::Timer(token) = event { - if self.pause_token == Some(token) { - match self.state { - State::PauseLeft => { - let anim = - Animation::new(self.max_offset, self.min_offset, self.duration, now); - self.state = State::Right(anim); - } - State::PauseRight => { - let anim = - Animation::new(self.min_offset, self.max_offset, self.duration, now); - self.state = State::Left(anim); - } - _ => {} + if self.pause_timer.expire(event) { + match self.state { + State::PauseLeft => { + let anim = Animation::new(self.max_offset, self.min_offset, self.duration, now); + self.state = State::Right(anim); + } + State::PauseRight => { + let anim = Animation::new(self.min_offset, self.max_offset, self.duration, now); + self.state = State::Left(anim); } + _ => {} + } + // We have something to paint, so request to be painted in the next pass. + ctx.request_paint(); + // There is further progress in the animation, request an animation frame event. + ctx.request_anim_frame(); + } + + if EventCtx::is_anim_frame(event) { + if self.is_animating() { // We have something to paint, so request to be painted in the next pass. ctx.request_paint(); - // There is further progress in the animation, request an animation frame event. + // There is further progress in the animation, request an animation frame + // event. ctx.request_anim_frame(); } - if token == EventCtx::ANIM_FRAME_TIMER { - if self.is_animating() { - // We have something to paint, so request to be painted in the next pass. - ctx.request_paint(); - // There is further progress in the animation, request an animation frame - // event. - ctx.request_anim_frame(); - } - - match self.state { - State::Right(_) => { - if self.is_at_right(now) { - self.pause_token = Some(ctx.request_timer(self.pause)); - self.state = State::PauseRight; - } + match self.state { + State::Right(_) => { + if self.is_at_right(now) { + self.pause_timer.start(ctx, self.pause); + self.state = State::PauseRight; } - State::Left(_) => { - if self.is_at_left(now) { - self.pause_token = Some(ctx.request_timer(self.pause)); - self.state = State::PauseLeft; - } + } + State::Left(_) => { + if self.is_at_left(now) { + self.pause_timer.start(ctx, self.pause); + self.state = State::PauseLeft; } - _ => {} } + _ => {} } } + None } diff --git a/core/embed/rust/src/ui/component/mod.rs b/core/embed/rust/src/ui/component/mod.rs index 32cea86d3ef..b6c2b8ad4a1 100644 --- a/core/embed/rust/src/ui/component/mod.rs +++ b/core/embed/rust/src/ui/component/mod.rs @@ -27,7 +27,7 @@ pub mod text; pub mod timeout; pub use bar::Bar; -pub use base::{Child, Component, ComponentExt, Event, EventCtx, FlowMsg, Never, TimerToken}; +pub use base::{Child, Component, ComponentExt, Event, EventCtx, FlowMsg, Never, Timer}; pub use border::Border; pub use button_request::{ButtonRequestExt, SendButtonRequest}; #[cfg(all(feature = "jpeg", feature = "ui_image_buffer", feature = "micropython"))] diff --git a/core/embed/rust/src/ui/component/paginated.rs b/core/embed/rust/src/ui/component/paginated.rs index c8c992c8a13..1622a5f2e44 100644 --- a/core/embed/rust/src/ui/component/paginated.rs +++ b/core/embed/rust/src/ui/component/paginated.rs @@ -1,4 +1,5 @@ /// Common message type for pagination components. +#[cfg_attr(feature = "debug", derive(ufmt::derive::uDebug))] pub enum PageMsg { /// Pass-through from paged component. Content(T), diff --git a/core/embed/rust/src/ui/component/timeout.rs b/core/embed/rust/src/ui/component/timeout.rs index bd0466c109a..9d5c83e6489 100644 --- a/core/embed/rust/src/ui/component/timeout.rs +++ b/core/embed/rust/src/ui/component/timeout.rs @@ -1,23 +1,22 @@ use crate::{ time::Duration, ui::{ - component::{Component, Event, EventCtx, TimerToken}, + component::{Component, Event, EventCtx, Timer}, geometry::Rect, shape::Renderer, }, }; -#[derive(Clone)] pub struct Timeout { time_ms: u32, - timer: Option, + timer: Timer, } impl Timeout { pub fn new(time_ms: u32) -> Self { Self { time_ms, - timer: None, + timer: Timer::new(), } } } @@ -30,19 +29,10 @@ impl Component for Timeout { } fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { - match event { - // Set up timer. - Event::Attach(_) => { - self.timer = Some(ctx.request_timer(Duration::from_millis(self.time_ms))); - None - } - // Fire. - Event::Timer(token) if Some(token) == self.timer => { - self.timer = None; - Some(()) - } - _ => None, + if matches!(event, Event::Attach(_)) { + self.timer.start(ctx, Duration::from_millis(self.time_ms)); } + self.timer.expire(event).then_some(()) } fn paint(&mut self) {} diff --git a/core/embed/rust/src/ui/flow/swipe.rs b/core/embed/rust/src/ui/flow/swipe.rs index 7113f9ca337..a26645abc93 100644 --- a/core/embed/rust/src/ui/flow/swipe.rs +++ b/core/embed/rust/src/ui/flow/swipe.rs @@ -11,12 +11,14 @@ use crate::{ Component, Event, EventCtx, FlowMsg, SwipeDetect, }, display::Color, - event::{SwipeEvent, TouchEvent}, + event::SwipeEvent, flow::{base::Decision, FlowController}, geometry::{Direction, Rect}, - layout::obj::ObjComponent, + layout::base::{Layout, LayoutState}, shape::{render_on_display, ConcreteRenderer, Renderer, ScopedRenderer}, + ui_features::ModelUI, util::animation_disabled, + UIFeaturesCommon, }, }; @@ -108,7 +110,11 @@ pub struct SwipeFlow { internal_pages: u16, /// If triggering swipe by event, make this decision instead of default /// after the swipe. - decision_override: Option, + pending_decision: Option, + /// Layout lifecycle state. + lifecycle_state: LayoutState, + /// Returned value from latest transition, stored as Obj. + returned_value: Option>, } impl SwipeFlow { @@ -120,7 +126,9 @@ impl SwipeFlow { allow_swipe: true, internal_state: 0, internal_pages: 1, - decision_override: None, + pending_decision: None, + lifecycle_state: LayoutState::Initial, + returned_value: None, }) } @@ -147,25 +155,36 @@ impl SwipeFlow { &mut self.store[self.state.index()] } - fn goto(&mut self, ctx: &mut EventCtx, attach_type: AttachType) { + fn update_page_count(&mut self, attach_type: AttachType) { + // update page count + self.internal_pages = self.current_page_mut().get_internal_page_count() as u16; + // reset internal state: + self.internal_state = if let Swipe(Direction::Down) = attach_type { + // if coming from below, set to the last page + self.internal_pages.saturating_sub(1) + } else { + // else reset to the first page + 0 + }; + } + + /// Transition to a different state. + /// + /// This is the only way to change the current flow state. + fn goto(&mut self, ctx: &mut EventCtx, new_state: FlowState, attach_type: AttachType) { + // update current page + self.state = new_state; + + // reset and unlock swipe config self.swipe = SwipeDetect::new(); + // unlock swipe events self.allow_swipe = true; + // send an Attach event to the new page self.current_page_mut() .event(ctx, Event::Attach(attach_type)); - self.internal_pages = self.current_page_mut().get_internal_page_count() as u16; - - match attach_type { - Swipe(Direction::Up) => { - self.internal_state = 0; - } - Swipe(Direction::Down) => { - self.internal_state = self.internal_pages.saturating_sub(1); - } - _ => {} - } - + self.update_page_count(attach_type); ctx.request_paint(); } @@ -187,10 +206,14 @@ impl SwipeFlow { } } - fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { let mut decision = Decision::Nothing; let mut return_transition: AttachType = AttachType::Initial; + if let Event::Attach(attach_type) = event { + self.update_page_count(attach_type); + } + let mut attach = false; let e = if self.allow_swipe { @@ -199,24 +222,23 @@ impl SwipeFlow { .get_swipe_config() .with_pagination(self.internal_state, self.internal_pages); - self.internal_pages = page.get_internal_page_count() as u16; - match self.swipe.event(ctx, event, config) { Some(SwipeEvent::End(dir)) => { - if let Some(override_decision) = self.decision_override.take() { - decision = override_decision; - } else { - decision = self.handle_swipe_child(ctx, dir); - } - return_transition = AttachType::Swipe(dir); let new_internal_state = config.paging_event(dir, self.internal_state, self.internal_pages); if new_internal_state != self.internal_state { + // internal paging event self.internal_state = new_internal_state; decision = Decision::Nothing; attach = true; + } else if let Some(override_decision) = self.pending_decision.take() { + // end of simulated swipe, applying original decision + decision = override_decision; + } else { + // normal end-of-swipe event handling + decision = self.handle_swipe_child(ctx, dir); } Event::Swipe(SwipeEvent::End(dir)) } @@ -253,32 +275,36 @@ impl SwipeFlow { if let Decision::Transition(_, Swipe(direction)) = decision { if config.is_allowed(direction) { + self.allow_swipe = true; if !animation_disabled() { self.swipe.trigger(ctx, direction, config); - self.decision_override = Some(decision); - decision = Decision::Nothing; + self.pending_decision = Some(decision); + return Some(LayoutState::Transitioning(return_transition)); } - self.allow_swipe = true; } } } _ => { //ignore message, we are already transitioning - self.current_page_mut().event(ctx, event); + let msg = self.current_page_mut().event(ctx, event); + assert!(msg.is_none()); } } match decision { Decision::Transition(new_state, attach) => { - self.state = new_state; - self.goto(ctx, attach); - None + self.goto(ctx, new_state, attach); + Some(LayoutState::Attached(ctx.button_request().take())) } Decision::Return(msg) => { ctx.set_transition_out(return_transition); self.swipe.reset(); self.allow_swipe = true; - Some(msg) + self.returned_value = Some(msg.try_into()); + Some(LayoutState::Done) + } + Decision::Nothing if matches!(event, Event::Attach(_)) => { + Some(LayoutState::Attached(ctx.button_request().take())) } _ => None, } @@ -296,23 +322,22 @@ impl SwipeFlow { /// This implementation relies on the fact that swipe components always return /// `FlowMsg` as their `Component::Msg` (provided by `impl FlowComponentTrait` /// earlier in this file). -#[cfg(feature = "micropython")] -impl ObjComponent for SwipeFlow { - fn obj_place(&mut self, bounds: Rect) -> Rect { +impl Layout> for SwipeFlow { + fn place(&mut self) { for elem in self.store.iter_mut() { - elem.place(bounds); + elem.place(ModelUI::SCREEN); } - bounds } - fn obj_event(&mut self, ctx: &mut EventCtx, event: Event) -> Result { - match self.event(ctx, event) { - None => Ok(Obj::const_none()), - Some(msg) => msg.try_into(), - } + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + self.event(ctx, event) + } + + fn value(&self) -> Option<&Result> { + self.returned_value.as_ref() } - fn obj_paint(&mut self) { + fn paint(&mut self) { render_on_display(None, Some(Color::black()), |target| { self.render_state(self.state.index(), target); }); diff --git a/core/embed/rust/src/ui/geometry.rs b/core/embed/rust/src/ui/geometry.rs index 50a9c1e275e..dde44132e46 100644 --- a/core/embed/rust/src/ui/geometry.rs +++ b/core/embed/rust/src/ui/geometry.rs @@ -579,6 +579,7 @@ impl Alignment2D { } #[derive(Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "debug", derive(ufmt::derive::uDebug))] pub enum Axis { Horizontal, Vertical, diff --git a/core/embed/rust/src/ui/layout/base.rs b/core/embed/rust/src/ui/layout/base.rs new file mode 100644 index 00000000000..71097ccb8df --- /dev/null +++ b/core/embed/rust/src/ui/layout/base.rs @@ -0,0 +1,84 @@ +use crate::ui::{ + button_request::ButtonRequest, + component::{base::AttachType, Event, EventCtx}, +}; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum LayoutState { + Initial, + Attached(Option), + Transitioning(AttachType), + Done, +} + +pub trait Layout { + //fn attach(&mut self, ctx: &mut EventCtx, attach_type: AttachType); + fn place(&mut self); + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option; + fn value(&self) -> Option<&T>; + fn paint(&mut self); +} + +#[cfg(feature = "micropython")] +mod micropython { + use crate::micropython::{ + macros::{obj_dict, obj_map, obj_type}, + obj::Obj, + qstr::Qstr, + simple_type::SimpleTypeObj, + typ::Type, + }; + + use super::LayoutState; + + static STATE_INITIAL_TYPE: Type = obj_type! { + name: Qstr::MP_QSTR_INITIAL, + base: LAYOUT_STATE_TYPE, + }; + + static STATE_ATTACHED_TYPE: Type = obj_type! { + name: Qstr::MP_QSTR_ATTACHED, + base: LAYOUT_STATE_TYPE, + }; + + static STATE_TRANSITIONING_TYPE: Type = obj_type! { + name: Qstr::MP_QSTR_TRANSITIONING, + base: LAYOUT_STATE_TYPE, + }; + + static STATE_DONE_TYPE: Type = obj_type! { + name: Qstr::MP_QSTR_DONE, + base: LAYOUT_STATE_TYPE, + }; + + pub static STATE_INITIAL: SimpleTypeObj = SimpleTypeObj::new(&STATE_INITIAL_TYPE); + pub static STATE_ATTACHED: SimpleTypeObj = SimpleTypeObj::new(&STATE_ATTACHED_TYPE); + pub static STATE_TRANSITIONING: SimpleTypeObj = SimpleTypeObj::new(&STATE_TRANSITIONING_TYPE); + pub static STATE_DONE: SimpleTypeObj = SimpleTypeObj::new(&STATE_DONE_TYPE); + + static LAYOUT_STATE_TYPE: Type = obj_type! { + name: Qstr::MP_QSTR_LayoutState, + locals: &obj_dict! { obj_map! { + Qstr::MP_QSTR_INITIAL => STATE_INITIAL.as_obj(), + Qstr::MP_QSTR_ATTACHED => STATE_ATTACHED.as_obj(), + Qstr::MP_QSTR_TRANSITIONING => STATE_TRANSITIONING.as_obj(), + Qstr::MP_QSTR_DONE => STATE_DONE.as_obj(), + } }, + }; + + pub static LAYOUT_STATE: SimpleTypeObj = SimpleTypeObj::new(&LAYOUT_STATE_TYPE); + + impl From for Obj { + fn from(state: LayoutState) -> Self { + match state { + LayoutState::Initial => STATE_INITIAL.as_obj(), + LayoutState::Attached(_) => STATE_ATTACHED.as_obj(), + LayoutState::Transitioning(_) => STATE_TRANSITIONING.as_obj(), + LayoutState::Done => STATE_DONE.as_obj(), + } + } + } +} + +#[cfg(feature = "micropython")] +pub use micropython::*; diff --git a/core/embed/rust/src/ui/layout/mod.rs b/core/embed/rust/src/ui/layout/mod.rs index 2126c59f16e..63b9ed1c3f1 100644 --- a/core/embed/rust/src/ui/layout/mod.rs +++ b/core/embed/rust/src/ui/layout/mod.rs @@ -1,3 +1,5 @@ +pub mod base; + #[cfg(feature = "micropython")] pub mod obj; diff --git a/core/embed/rust/src/ui/layout/obj.rs b/core/embed/rust/src/ui/layout/obj.rs index e27ddf8df26..2c1f4115c51 100644 --- a/core/embed/rust/src/ui/layout/obj.rs +++ b/core/embed/rust/src/ui/layout/obj.rs @@ -1,8 +1,10 @@ use core::{ cell::{RefCell, RefMut}, convert::{TryFrom, TryInto}, + marker::PhantomData, ops::{Deref, DerefMut}, }; +#[cfg(feature = "touch")] use num_traits::{FromPrimitive, ToPrimitive}; #[cfg(feature = "button")] @@ -28,13 +30,19 @@ use crate::{ time::Duration, ui::{ button_request::ButtonRequest, - component::{base::AttachType, Component, Event, EventCtx, Never, TimerToken}, - constant, display, + component::{ + base::{AttachType, TimerToken}, + Component, Event, EventCtx, Never, + }, + display, event::USBEvent, - geometry::Rect, + ui_features::ModelUI, + UIFeaturesCommon, }, }; +use super::base::{Layout, LayoutState}; + impl AttachType { fn to_obj(self) -> Obj { match self { @@ -82,52 +90,83 @@ pub trait ComponentMsgObj: Component { fn msg_try_into_obj(&self, msg: Self::Msg) -> Result; } -/// Object-safe interface between trait `Component` and MicroPython world. It -/// converts the result of `Component::event` into `Obj` via the -/// `ComponentMsgObj` trait, in order to easily return the value to Python. It -/// also optionally implies `Trace` for UI debugging. -/// Note: we need to use an object-safe trait in order to store it in a `Gc` field. `Component` itself is not object-safe because of `Component::Msg` -/// associated type. -pub trait ObjComponent: MaybeTrace { - fn obj_place(&mut self, bounds: Rect) -> Rect; - fn obj_event(&mut self, ctx: &mut EventCtx, event: Event) -> Result; - fn obj_paint(&mut self); - fn obj_bounds(&self, _sink: &mut dyn FnMut(Rect)) {} +pub trait ComponentMaybeTrace: Component + ComponentMsgObj + MaybeTrace {} +impl ComponentMaybeTrace for T where T: Component + ComponentMsgObj + MaybeTrace {} + +struct RootComponent +where + T: Component, + M: UIFeaturesCommon, +{ + inner: T, + returned_value: Option>, + _features: PhantomData, } -impl ObjComponent for T +impl RootComponent where - T: Component + ComponentMsgObj + MaybeTrace, + T: ComponentMaybeTrace, + M: UIFeaturesCommon, { - fn obj_place(&mut self, bounds: Rect) -> Rect { - self.place(bounds) + pub fn new(component: T) -> Self { + Self { + inner: component, + returned_value: None, + _features: PhantomData, + } } +} - fn obj_event(&mut self, ctx: &mut EventCtx, event: Event) -> Result { - if let Some(msg) = self.event(ctx, event) { - self.msg_try_into_obj(msg) +impl Layout> for RootComponent +where + T: Component + ComponentMsgObj, +{ + fn place(&mut self) { + self.inner.place(ModelUI::SCREEN); + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + if let Some(msg) = self.inner.event(ctx, event) { + self.returned_value = Some(self.inner.msg_try_into_obj(msg)); + Some(LayoutState::Done) + } else if matches!(event, Event::Attach(_)) { + Some(LayoutState::Attached(ctx.button_request().take())) } else { - Ok(Obj::const_none()) + None } } - fn obj_paint(&mut self) { - #[cfg(not(feature = "new_rendering"))] - { - self.paint(); - } + fn value(&self) -> Option<&Result> { + self.returned_value.as_ref() + } + fn paint(&mut self) { + #[cfg(not(feature = "new_rendering"))] + self.inner.paint(); #[cfg(feature = "new_rendering")] { render_on_display(None, Some(Color::black()), |target| { - self.render(target); + self.inner.render(target); }); } } } +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for RootComponent +where + T: Component + crate::trace::Trace, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + self.inner.trace(t); + } +} + +pub trait LayoutMaybeTrace: Layout> + MaybeTrace {} +impl LayoutMaybeTrace for T where T: Layout> + MaybeTrace {} + #[derive(Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "debug", derive(ufmt::derive::uDebug))] enum Repaint { None, Partial, @@ -144,28 +183,38 @@ pub struct LayoutObj { } struct LayoutObjInner { - root: Option>, + root: Option>, event_ctx: EventCtx, timer_fn: Obj, page_count: u16, repaint: Repaint, transition_out: AttachType, + button_request: Option, } impl LayoutObjInner { /// Create a new `LayoutObj`, wrapping a root component. #[inline(never)] - pub fn new(root: impl ObjComponent + 'static) -> Result { + pub fn new(root: impl LayoutMaybeTrace + 'static) -> Result { let root = GcBox::new(root)?; - Ok(Self { - root: Some(gc::coerce!(ObjComponent, root)), + let mut new = Self { + root: Some(gc::coerce!(LayoutMaybeTrace, root)), event_ctx: EventCtx::new(), timer_fn: Obj::const_none(), page_count: 1, repaint: Repaint::Full, transition_out: AttachType::Initial, - }) + button_request: None, + }; + + // invoke the initial placement + new.root_mut().place(); + // cause a repaint pass to update the number of pages + let msg = new.obj_event(Event::RequestPaint); + assert!(matches!(msg, Ok(s) if s == Obj::const_none())); + + Ok(new) } fn obj_delete(&mut self) { @@ -178,24 +227,22 @@ impl LayoutObjInner { self.timer_fn = timer_fn; } - fn root(&self) -> &impl Deref { + fn root(&self) -> &impl Deref { unwrap!(self.root.as_ref()) } - fn root_mut(&mut self) -> &mut impl DerefMut { + fn root_mut(&mut self) -> &mut impl DerefMut { unwrap!(self.root.as_mut()) } fn obj_request_repaint(&mut self) { self.repaint = Repaint::Full; - let mut dummy_ctx = EventCtx::new(); - let paint_msg = self - .root_mut() - .obj_event(&mut dummy_ctx, Event::RequestPaint); - // paint_msg must not be an error and it must not return a result - assert!(matches!(paint_msg, Ok(s) if s == Obj::const_none())); + let mut event_ctx = EventCtx::new(); + let paint_msg = self.root_mut().event(&mut event_ctx, Event::RequestPaint); + // paint_msg must not change the state + assert!(paint_msg.is_none()); // there must be no timers set - assert!(dummy_ctx.pop_timer().is_none()); + assert!(event_ctx.pop_timer().is_none()); } /// Run an event pass over the component tree. After the traversal, any @@ -204,21 +251,32 @@ impl LayoutObjInner { /// an error, `Ok` with the message otherwise. fn obj_event(&mut self, event: Event) -> Result { let root = unwrap!(self.root.as_mut()); - // Place the root component on the screen in case it was previously requested. - if self.event_ctx.needs_place() { - root.obj_place(constant::screen()); - } - // Clear the leftover flags from the previous event pass. + // Get the event context ready for a new event self.event_ctx.clear(); - // Send the event down the component tree. Bail out in case of failure. - let msg = root.obj_event(&mut self.event_ctx, event)?; + // Send the event down the component tree. + let msg = root.event(&mut self.event_ctx, event); + + match msg { + Some(LayoutState::Done) => return Ok(msg.into()), // short-circuit + Some(LayoutState::Attached(br)) => { + assert!(self.button_request.is_none()); + self.button_request = br; + } + Some(LayoutState::Transitioning(t)) => self.transition_out = t, + _ => (), + }; + + // Place the root component on the screen in case it was requested. + if self.event_ctx.needs_place() { + root.place(); + } // Check if we should repaint next time if self.event_ctx.needs_repaint_root() { self.obj_request_repaint(); - } else if self.event_ctx.needs_repaint() { + } else if self.event_ctx.needs_repaint() && self.repaint == Repaint::None { self.repaint = Repaint::Partial; } @@ -236,15 +294,12 @@ impl LayoutObjInner { } } + // Update page count if it changed if let Some(count) = self.event_ctx.page_count() { self.page_count = count as u16; } - if let Some(t) = self.event_ctx.get_transition_out() { - self.transition_out = t; - } - - Ok(msg) + Ok(msg.into()) } /// Run a paint pass over the component tree. Returns true if any component @@ -254,16 +309,11 @@ impl LayoutObjInner { display::clear(); } - // Place the root component on the screen in case it was previously requested. - if self.event_ctx.needs_place() { - self.root_mut().obj_place(constant::screen()); - } - display::sync(); if self.repaint != Repaint::None { self.repaint = Repaint::None; - self.root_mut().obj_paint(); + self.root_mut().paint(); true } else { false @@ -284,10 +334,10 @@ impl LayoutObjInner { // For Reasons(tm), we must pass a closure in which we call `root.trace(t)`, // instead of passing `root` into the tracer. - // (The Reasons being, root is a `Gc`, and `Gc` does not - // implement `Trace`, and `dyn ObjComponent` is unsized so we can't deref it to - // claim that it implements `Trace`, and we also can't upcast it to `&dyn Trace` - // because trait upcasting is unstable. + // (The Reasons being, root is a `Gc`, and `Gc` does not + // implement `Trace`, and `dyn LayoutMaybeTrace` is unsized so we can't deref it + // to claim that it implements `Trace`, and we also can't upcast it to + // `&dyn Trace` because trait upcasting is unstable. // Luckily, calling `root.trace()` works perfectly fine in spite of the above.) tracer.root(&|t| { self.root().trace(t); @@ -299,7 +349,7 @@ impl LayoutObjInner { } fn obj_button_request(&mut self) -> Result { - match self.event_ctx.button_request() { + match self.button_request.take() { None => Ok(Obj::const_none()), Some(ButtonRequest { code, name }) => (code.num().into(), name.try_into()?).try_into(), } @@ -308,11 +358,23 @@ impl LayoutObjInner { fn obj_get_transition_out(&self) -> Obj { self.transition_out.to_obj() } + + fn obj_return_value(&self) -> Result { + self.root() + .value() + .cloned() + .unwrap_or(Ok(Obj::const_none())) + } } impl LayoutObj { /// Create a new `LayoutObj`, wrapping a root component. - pub fn new(root: impl ObjComponent + 'static) -> Result, Error> { + pub fn new(root: T) -> Result, Error> { + let root_component = RootComponent::new(root); + Self::new_root(root_component) + } + + pub fn new_root(root: impl LayoutMaybeTrace + 'static) -> Result, Error> { // SAFETY: This is a Python object and hase a base as first element unsafe { Gc::new_with_custom_finaliser(Self { @@ -343,6 +405,7 @@ impl LayoutObj { Qstr::MP_QSTR_page_count => obj_fn_1!(ui_layout_page_count).as_obj(), Qstr::MP_QSTR_button_request => obj_fn_1!(ui_layout_button_request).as_obj(), Qstr::MP_QSTR_get_transition_out => obj_fn_1!(ui_layout_get_transition_out).as_obj(), + Qstr::MP_QSTR_return_value => obj_fn_1!(ui_layout_return_value).as_obj(), }), }; &TYPE @@ -420,9 +483,8 @@ extern "C" fn ui_layout_attach_timer_fn(this: Obj, timer_fn: Obj, attach_type: O let msg = this .inner_mut() - .obj_event(Event::Attach(AttachType::try_from_obj(attach_type)?))?; - assert!(msg == Obj::const_none()); - Ok(Obj::const_none()) + .obj_event(Event::Attach(AttachType::try_from_obj(attach_type)?)); + msg }; unsafe { util::try_or_raise(block) } } @@ -553,6 +615,15 @@ extern "C" fn ui_layout_get_transition_out(this: Obj) -> Obj { unsafe { util::try_or_raise(block) } } +extern "C" fn ui_layout_return_value(this: Obj) -> Obj { + let block = || { + let this: Gc = this.try_into()?; + let value = this.inner_mut().obj_return_value(); + value + }; + unsafe { util::try_or_raise(block) } +} + #[cfg(feature = "ui_debug")] #[no_mangle] pub extern "C" fn ui_debug_layout_type() -> &'static Type { diff --git a/core/embed/rust/src/ui/model_mercury/component/address_details.rs b/core/embed/rust/src/ui/model_mercury/component/address_details.rs index 928e81567e0..7e2978a5a58 100644 --- a/core/embed/rust/src/ui/model_mercury/component/address_details.rs +++ b/core/embed/rust/src/ui/model_mercury/component/address_details.rs @@ -21,7 +21,6 @@ use super::{theme, Frame, FrameMsg}; const MAX_XPUBS: usize = 16; -#[derive(Clone)] pub struct AddressDetails { details: Frame>>, xpub_view: Frame>>, diff --git a/core/embed/rust/src/ui/model_mercury/component/binary_selection.rs b/core/embed/rust/src/ui/model_mercury/component/binary_selection.rs index b1a8495e68f..5b990c0926b 100644 --- a/core/embed/rust/src/ui/model_mercury/component/binary_selection.rs +++ b/core/embed/rust/src/ui/model_mercury/component/binary_selection.rs @@ -13,7 +13,6 @@ pub enum BinarySelectionMsg { /// Component presenting a binary choice represented as two buttons, left and /// right. Both buttons are parameterized with content and style. -#[derive(Clone)] pub struct BinarySelection { buttons_area: Rect, button_left: Button, diff --git a/core/embed/rust/src/ui/model_mercury/component/button.rs b/core/embed/rust/src/ui/model_mercury/component/button.rs index a68cb4f77f6..2d2d0282e95 100644 --- a/core/embed/rust/src/ui/model_mercury/component/button.rs +++ b/core/embed/rust/src/ui/model_mercury/component/button.rs @@ -4,12 +4,11 @@ use crate::{ strutil::TString, time::Duration, ui::{ - component::{Component, Event, EventCtx, TimerToken}, + component::{Component, Event, EventCtx, Timer}, display::{self, toif::Icon, Color, Font}, event::TouchEvent, geometry::{Alignment, Alignment2D, Insets, Offset, Point, Rect}, - shape, - shape::Renderer, + shape::{self, Renderer}, util::split_two_lines, }, }; @@ -23,7 +22,6 @@ pub enum ButtonMsg { LongPressed, } -#[derive(Clone)] pub struct Button { area: Rect, touch_expand: Option, @@ -33,7 +31,7 @@ pub struct Button { radius: Option, state: State, long_press: Option, - long_timer: Option, + long_timer: Timer, haptic: bool, } @@ -53,7 +51,7 @@ impl Button { radius: None, state: State::Initial, long_press: None, - long_timer: None, + long_timer: Timer::new(), haptic: true, } } @@ -312,7 +310,7 @@ impl Component for Button { } self.set(ctx, State::Pressed); if let Some(duration) = self.long_press { - self.long_timer = Some(ctx.request_timer(duration)); + self.long_timer.start(ctx, duration); } return Some(ButtonMsg::Pressed); } @@ -344,13 +342,13 @@ impl Component for Button { State::Pressed => { // Touch finished outside our area. self.set(ctx, State::Initial); - self.long_timer = None; + self.long_timer.stop(); return Some(ButtonMsg::Released); } _ => { // Touch finished outside our area. self.set(ctx, State::Initial); - self.long_timer = None; + self.long_timer.stop(); } } } @@ -363,28 +361,25 @@ impl Component for Button { State::Pressed => { // Touch aborted self.set(ctx, State::Initial); - self.long_timer = None; + self.long_timer.stop(); return Some(ButtonMsg::Released); } _ => { // Irrelevant touch abort self.set(ctx, State::Initial); - self.long_timer = None; + self.long_timer.stop(); } } } - Event::Timer(token) => { - if self.long_timer == Some(token) { - self.long_timer = None; - if matches!(self.state, State::Pressed) { - #[cfg(feature = "haptic")] - if self.haptic { - play(HapticEffect::ButtonPress); - } - self.set(ctx, State::Initial); - return Some(ButtonMsg::LongPressed); + Event::Timer(_) if self.long_timer.expire(event) => { + if matches!(self.state, State::Pressed) { + #[cfg(feature = "haptic")] + if self.haptic { + play(HapticEffect::ButtonPress); } + self.set(ctx, State::Initial); + return Some(ButtonMsg::LongPressed); } } _ => {} diff --git a/core/embed/rust/src/ui/model_mercury/component/frame.rs b/core/embed/rust/src/ui/model_mercury/component/frame.rs index 1e6aa12f484..6825da34ff5 100644 --- a/core/embed/rust/src/ui/model_mercury/component/frame.rs +++ b/core/embed/rust/src/ui/model_mercury/component/frame.rs @@ -79,7 +79,6 @@ impl HorizontalSwipe { } } -#[derive(Clone)] pub struct Frame { bounds: Rect, content: T, diff --git a/core/embed/rust/src/ui/model_mercury/component/header.rs b/core/embed/rust/src/ui/model_mercury/component/header.rs index 8b22248eda5..45a3d48cf1a 100644 --- a/core/embed/rust/src/ui/model_mercury/component/header.rs +++ b/core/embed/rust/src/ui/model_mercury/component/header.rs @@ -59,7 +59,7 @@ impl AttachAnimation { } const BUTTON_EXPAND_BORDER: i16 = 32; -#[derive(Clone)] + pub struct Header { area: Rect, title: Label<'static>, diff --git a/core/embed/rust/src/ui/model_mercury/component/hold_to_confirm.rs b/core/embed/rust/src/ui/model_mercury/component/hold_to_confirm.rs index 258882d9e81..f0f2b145e69 100644 --- a/core/embed/rust/src/ui/model_mercury/component/hold_to_confirm.rs +++ b/core/embed/rust/src/ui/model_mercury/component/hold_to_confirm.rs @@ -163,7 +163,6 @@ impl HoldToConfirmAnim { /// Component requesting a hold to confirm action from a user. Most typically /// embedded as a content of a Frame. -#[derive(Clone)] pub struct HoldToConfirm { title: Label<'static>, area: Rect, diff --git a/core/embed/rust/src/ui/model_mercury/component/homescreen.rs b/core/embed/rust/src/ui/model_mercury/component/homescreen.rs index c025b0b3336..892cebfc966 100644 --- a/core/embed/rust/src/ui/model_mercury/component/homescreen.rs +++ b/core/embed/rust/src/ui/model_mercury/component/homescreen.rs @@ -5,7 +5,7 @@ use crate::{ translations::TR, trezorhal::usb::usb_configured, ui::{ - component::{Component, Event, EventCtx, TimerToken}, + component::{Component, Event, EventCtx, Timer}, display::{image::ImageInfo, Color, Font}, event::{TouchEvent, USBEvent}, geometry::{Alignment, Alignment2D, Offset, Point, Rect}, @@ -235,8 +235,8 @@ impl AttachAnimation { } struct HideLabelAnimation { - pub timer: Stopwatch, - token: TimerToken, + pub stopwatch: Stopwatch, + timer: Timer, animating: bool, hidden: bool, duration: Duration, @@ -260,8 +260,8 @@ impl HideLabelAnimation { fn new(label_width: i16) -> Self { Self { - timer: Stopwatch::default(), - token: TimerToken::INVALID, + stopwatch: Stopwatch::default(), + timer: Timer::new(), animating: false, hidden: false, duration: Duration::from_millis((label_width as u32 * 300) / 120), @@ -269,19 +269,19 @@ impl HideLabelAnimation { } fn is_active(&self) -> bool { - self.timer.is_running_within(self.duration) + self.stopwatch.is_running_within(self.duration) } fn reset(&mut self) { - self.timer = Stopwatch::default(); + self.stopwatch = Stopwatch::default(); } fn elapsed(&self) -> Duration { - self.timer.elapsed() + self.stopwatch.elapsed() } fn change_dir(&mut self) { - let elapsed = self.timer.elapsed(); + let elapsed = self.stopwatch.elapsed(); let start = self .duration @@ -289,9 +289,9 @@ impl HideLabelAnimation { .and_then(|e| Instant::now().checked_sub(e)); if let Some(start) = start { - self.timer = Stopwatch::Running(start); + self.stopwatch = Stopwatch::Running(start); } else { - self.timer = Stopwatch::new_started(); + self.stopwatch = Stopwatch::new_started(); } } @@ -300,7 +300,7 @@ impl HideLabelAnimation { return Offset::zero(); } - let t = self.timer.elapsed().to_millis() as f32 / 1000.0; + let t = self.stopwatch.elapsed().to_millis() as f32 / 1000.0; let pos = if self.hidden { pareen::constant(0.0) @@ -329,7 +329,7 @@ impl HideLabelAnimation { match event { Event::Attach(AttachType::Initial) => { ctx.request_anim_frame(); - self.token = ctx.request_timer(Self::HIDE_AFTER); + self.timer.start(ctx, Self::HIDE_AFTER); } Event::Attach(AttachType::Resume) => { self.hidden = resume.hidden; @@ -341,13 +341,13 @@ impl HideLabelAnimation { self.animating = resume.animating; if self.animating { - self.timer = Stopwatch::Running(start); + self.stopwatch = Stopwatch::Running(start); ctx.request_anim_frame(); } else { - self.timer = Stopwatch::new_stopped(); + self.stopwatch = Stopwatch::new_stopped(); } if !self.animating && !self.hidden { - self.token = ctx.request_timer(Self::HIDE_AFTER); + self.timer.start(ctx, Self::HIDE_AFTER); } } Event::Timer(EventCtx::ANIM_FRAME_TIMER) => { @@ -361,28 +361,26 @@ impl HideLabelAnimation { ctx.request_paint(); if !self.hidden { - self.token = ctx.request_timer(Self::HIDE_AFTER); + self.timer.start(ctx, Self::HIDE_AFTER); } } } - Event::Timer(token) => { - if token == self.token && !animation_disabled() { - self.timer.start(); - ctx.request_anim_frame(); - self.animating = true; - self.hidden = false; - } + Event::Timer(_) if self.timer.expire(event) && !animation_disabled() => { + self.stopwatch.start(); + ctx.request_anim_frame(); + self.animating = true; + self.hidden = false; } Event::Touch(TouchEvent::TouchStart(_)) => { if !self.animating { if self.hidden { - self.timer.start(); + self.stopwatch.start(); self.animating = true; ctx.request_anim_frame(); ctx.request_paint(); } else { - self.token = ctx.request_timer(Self::HIDE_AFTER); + self.timer.start(ctx, Self::HIDE_AFTER); } } else if !self.hidden { self.change_dir(); @@ -399,7 +397,7 @@ impl HideLabelAnimation { HideLabelAnimationState { animating: self.animating, hidden: self.hidden, - elapsed: self.timer.elapsed().to_millis(), + elapsed: self.stopwatch.elapsed().to_millis(), } } } @@ -420,7 +418,7 @@ pub struct Homescreen { bg_image: ImageBuffer>, hold_to_lock: bool, loader: Loader, - delay: Option, + delay: Timer, attach_animation: AttachAnimation, label_anim: HideLabelAnimation, } @@ -458,7 +456,7 @@ impl Homescreen { bg_image: buf, hold_to_lock, loader: Loader::with_lock_icon().with_durations(LOADER_DURATION, LOADER_DURATION / 3), - delay: None, + delay: Timer::new(), attach_animation: AttachAnimation::default(), label_anim: HideLabelAnimation::new(label_width), } @@ -513,11 +511,11 @@ impl Homescreen { if self.loader.is_animating() { self.loader.start_growing(ctx, Instant::now()); } else { - self.delay = Some(ctx.request_timer(LOADER_DELAY)); + self.delay.start(ctx, LOADER_DELAY); } } Event::Touch(TouchEvent::TouchEnd(_)) => { - self.delay = None; + self.delay.stop(); let now = Instant::now(); if self.loader.is_completely_grown(now) { return true; @@ -526,8 +524,7 @@ impl Homescreen { self.loader.start_shrinking(ctx, now); } } - Event::Timer(token) if Some(token) == self.delay => { - self.delay = None; + Event::Timer(_) if self.delay.expire(event) => { self.loader.start_growing(ctx, Instant::now()); } _ => {} diff --git a/core/embed/rust/src/ui/model_mercury/component/keyboard/bip39.rs b/core/embed/rust/src/ui/model_mercury/component/keyboard/bip39.rs index 582092778fd..9eec4b179bc 100644 --- a/core/embed/rust/src/ui/model_mercury/component/keyboard/bip39.rs +++ b/core/embed/rust/src/ui/model_mercury/component/keyboard/bip39.rs @@ -93,7 +93,7 @@ impl Component for Bip39Input { fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { self.button_suggestion.event(ctx, event); - if self.multi_tap.is_timeout_event(event) { + if self.multi_tap.timeout_event(event) { self.on_timeout(ctx) } else if let Some(ButtonMsg::Clicked) = self.button.event(ctx, event) { self.on_input_click(ctx) diff --git a/core/embed/rust/src/ui/model_mercury/component/keyboard/common.rs b/core/embed/rust/src/ui/model_mercury/component/keyboard/common.rs index 628e0705fd9..0f52d68632f 100644 --- a/core/embed/rust/src/ui/model_mercury/component/keyboard/common.rs +++ b/core/embed/rust/src/ui/model_mercury/component/keyboard/common.rs @@ -1,11 +1,10 @@ use crate::{ time::Duration, ui::{ - component::{text::common::TextEdit, Event, EventCtx, TimerToken}, + component::{text::common::TextEdit, Event, EventCtx, Timer}, display::{self, Color, Font}, geometry::{Alignment2D, Offset, Point, Rect}, - shape, - shape::Renderer, + shape::{self, Renderer}, }, }; @@ -17,6 +16,8 @@ pub struct MultiTapKeyboard { timeout: Duration, /// The currently pending state. pending: Option, + /// Timer for clearing the pending state. + timer: Timer, } struct Pending { @@ -25,8 +26,6 @@ struct Pending { /// Index of the key press (how many times the `key` was pressed, minus /// one). press: usize, - /// Timer for clearing the pending state. - timer: TimerToken, } impl MultiTapKeyboard { @@ -35,6 +34,7 @@ impl MultiTapKeyboard { Self { timeout: Duration::from_secs(1), pending: None, + timer: Timer::new(), } } @@ -48,21 +48,17 @@ impl MultiTapKeyboard { self.pending.as_ref().map(|p| p.press) } - /// Return the token for the currently pending timer. - pub fn pending_timer(&self) -> Option { - self.pending.as_ref().map(|p| p.timer) - } - /// Returns `true` if `event` is an `Event::Timer` for the currently pending /// timer. - pub fn is_timeout_event(&self, event: Event) -> bool { - matches!((event, self.pending_timer()), (Event::Timer(t), Some(pt)) if pt == t) + pub fn timeout_event(&mut self, event: Event) -> bool { + self.timer.expire(event) } /// Reset to the empty state. Takes `EventCtx` to request a paint pass (to /// either hide or show any pending marker our caller might want to draw /// later). pub fn clear_pending_state(&mut self, ctx: &mut EventCtx) { + self.timer.stop(); if self.pending.is_some() { self.pending = None; ctx.request_paint(); @@ -97,11 +93,8 @@ impl MultiTapKeyboard { // transition only happens as a result of an append op, so the painting should // be requested by handling the `TextEdit`. self.pending = if key_text.len() > 1 { - Some(Pending { - key, - press, - timer: ctx.request_timer(self.timeout), - }) + self.timer.start(ctx, self.timeout); + Some(Pending { key, press }) } else { None }; diff --git a/core/embed/rust/src/ui/model_mercury/component/keyboard/passphrase.rs b/core/embed/rust/src/ui/model_mercury/component/keyboard/passphrase.rs index afca80497ef..52651ff06dd 100644 --- a/core/embed/rust/src/ui/model_mercury/component/keyboard/passphrase.rs +++ b/core/embed/rust/src/ui/model_mercury/component/keyboard/passphrase.rs @@ -321,7 +321,7 @@ impl Component for PassphraseKeyboard { } fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { - if self.input.multi_tap.is_timeout_event(event) { + if self.input.multi_tap.timeout_event(event) { self.input.multi_tap.clear_pending_state(ctx); return None; } diff --git a/core/embed/rust/src/ui/model_mercury/component/keyboard/pin.rs b/core/embed/rust/src/ui/model_mercury/component/keyboard/pin.rs index 62c3b9137ca..6561ad802eb 100644 --- a/core/embed/rust/src/ui/model_mercury/component/keyboard/pin.rs +++ b/core/embed/rust/src/ui/model_mercury/component/keyboard/pin.rs @@ -8,7 +8,7 @@ use crate::{ component::{ base::{AttachType, ComponentExt}, text::TextStyle, - Component, Event, EventCtx, Label, Never, Pad, TimerToken, + Component, Event, EventCtx, Label, Never, Pad, Timer, }, display::Font, event::TouchEvent, @@ -256,7 +256,7 @@ pub struct PinKeyboard<'a> { cancel_btn: Button, confirm_btn: Button, digit_btns: [(Button, usize); DIGIT_COUNT], - warning_timer: Option, + warning_timer: Timer, attach_animation: AttachAnimation, close_animation: CloseAnimation, close_confirm: bool, @@ -295,7 +295,7 @@ impl<'a> PinKeyboard<'a> { .styled(theme::button_pin_confirm()) .initially_enabled(false), digit_btns: Self::generate_digit_buttons(), - warning_timer: None, + warning_timer: Timer::new(), attach_animation: AttachAnimation::default(), close_animation: CloseAnimation::default(), close_confirm: false, @@ -417,10 +417,10 @@ impl Component for PinKeyboard<'_> { match event { // Set up timer to switch off warning prompt. Event::Attach(_) if self.major_warning.is_some() => { - self.warning_timer = Some(ctx.request_timer(Duration::from_secs(2))); + self.warning_timer.start(ctx, Duration::from_secs(2)); } // Hide warning, show major prompt. - Event::Timer(token) if Some(token) == self.warning_timer => { + Event::Timer(_) if self.warning_timer.expire(event) => { self.major_warning = None; self.minor_prompt.request_complete_repaint(ctx); ctx.request_paint(); diff --git a/core/embed/rust/src/ui/model_mercury/component/keyboard/slip39.rs b/core/embed/rust/src/ui/model_mercury/component/keyboard/slip39.rs index abd6bcadd76..de7ec4f1a70 100644 --- a/core/embed/rust/src/ui/model_mercury/component/keyboard/slip39.rs +++ b/core/embed/rust/src/ui/model_mercury/component/keyboard/slip39.rs @@ -106,7 +106,7 @@ impl Component for Slip39Input { } fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { - if self.multi_tap.is_timeout_event(event) { + if self.multi_tap.timeout_event(event) { // Timeout occurred. Reset the pending key. self.multi_tap.clear_pending_state(ctx); return Some(MnemonicInputMsg::TimedOut); diff --git a/core/embed/rust/src/ui/model_mercury/component/prompt_screen.rs b/core/embed/rust/src/ui/model_mercury/component/prompt_screen.rs index 214f603c486..9737523debb 100644 --- a/core/embed/rust/src/ui/model_mercury/component/prompt_screen.rs +++ b/core/embed/rust/src/ui/model_mercury/component/prompt_screen.rs @@ -11,7 +11,6 @@ use super::{super::theme, BinarySelection, ButtonContent, HoldToConfirm, TapToCo /// - Tap to confirm /// - Hold to confirm /// - Yes/No selection -#[derive(Clone)] pub enum PromptScreen { Tap(TapToConfirm), Hold(HoldToConfirm), diff --git a/core/embed/rust/src/ui/model_mercury/component/status_screen.rs b/core/embed/rust/src/ui/model_mercury/component/status_screen.rs index d4145a30b48..d9cff5b86cc 100644 --- a/core/embed/rust/src/ui/model_mercury/component/status_screen.rs +++ b/core/embed/rust/src/ui/model_mercury/component/status_screen.rs @@ -129,7 +129,6 @@ impl StatusAnimation { /// Component showing status of an operation. Most typically embedded as a /// content of a Frame and showing success (checkmark with a circle around). -#[derive(Clone)] pub struct StatusScreen { area: Rect, icon: Icon, @@ -140,7 +139,6 @@ pub struct StatusScreen { msg: Label<'static>, } -#[derive(Clone)] enum DismissType { SwipeUp, Timeout(Timeout), diff --git a/core/embed/rust/src/ui/model_mercury/component/tap_to_confirm.rs b/core/embed/rust/src/ui/model_mercury/component/tap_to_confirm.rs index abad6b797c2..184e5fdf71c 100644 --- a/core/embed/rust/src/ui/model_mercury/component/tap_to_confirm.rs +++ b/core/embed/rust/src/ui/model_mercury/component/tap_to_confirm.rs @@ -113,7 +113,6 @@ impl TapToConfirmAnim { /// Component requesting a Tap to confirm action from a user. Most typically /// embedded as a content of a Frame. -#[derive(Clone)] pub struct TapToConfirm { area: Rect, button: Button, diff --git a/core/embed/rust/src/ui/model_mercury/component/vertical_menu.rs b/core/embed/rust/src/ui/model_mercury/component/vertical_menu.rs index bc82d304a39..f8d423997b6 100644 --- a/core/embed/rust/src/ui/model_mercury/component/vertical_menu.rs +++ b/core/embed/rust/src/ui/model_mercury/component/vertical_menu.rs @@ -167,7 +167,6 @@ impl AttachAnimation { } } -#[derive(Clone)] pub struct VerticalMenu { /// buttons placed vertically from top to bottom buttons: VerticalMenuButtons, diff --git a/core/embed/rust/src/ui/model_mercury/flow/confirm_action.rs b/core/embed/rust/src/ui/model_mercury/flow/confirm_action.rs index 0e2913f3200..682802fd4eb 100644 --- a/core/embed/rust/src/ui/model_mercury/flow/confirm_action.rs +++ b/core/embed/rust/src/ui/model_mercury/flow/confirm_action.rs @@ -225,7 +225,7 @@ fn create_menu_and_confirm( let flow = create_confirm(flow, subtitle, hold, prompt_screen)?; - Ok(LayoutObj::new(flow)?.into()) + Ok(LayoutObj::new_root(flow)?.into()) } fn create_menu( diff --git a/core/embed/rust/src/ui/model_mercury/flow/confirm_fido.rs b/core/embed/rust/src/ui/model_mercury/flow/confirm_fido.rs index 24a5991c61b..7e569d23738 100644 --- a/core/embed/rust/src/ui/model_mercury/flow/confirm_fido.rs +++ b/core/embed/rust/src/ui/model_mercury/flow/confirm_fido.rs @@ -214,6 +214,6 @@ impl ConfirmFido { .with_page(&ConfirmFido::Details, content_details)? .with_page(&ConfirmFido::Tap, content_tap)? .with_page(&ConfirmFido::Menu, content_menu)?; - Ok(LayoutObj::new(res)?.into()) + Ok(LayoutObj::new_root(res)?.into()) } } diff --git a/core/embed/rust/src/ui/model_mercury/flow/confirm_firmware_update.rs b/core/embed/rust/src/ui/model_mercury/flow/confirm_firmware_update.rs index 0beb2484a91..be0a9cb14fe 100644 --- a/core/embed/rust/src/ui/model_mercury/flow/confirm_firmware_update.rs +++ b/core/embed/rust/src/ui/model_mercury/flow/confirm_firmware_update.rs @@ -137,6 +137,6 @@ impl ConfirmFirmwareUpdate { .with_page(&ConfirmFirmwareUpdate::Menu, content_menu)? .with_page(&ConfirmFirmwareUpdate::Fingerprint, content_fingerprint)? .with_page(&ConfirmFirmwareUpdate::Confirm, content_confirm)?; - Ok(LayoutObj::new(res)?.into()) + Ok(LayoutObj::new_root(res)?.into()) } } diff --git a/core/embed/rust/src/ui/model_mercury/flow/confirm_output.rs b/core/embed/rust/src/ui/model_mercury/flow/confirm_output.rs index 5aa3b2a8f86..51c7b9b4b7e 100644 --- a/core/embed/rust/src/ui/model_mercury/flow/confirm_output.rs +++ b/core/embed/rust/src/ui/model_mercury/flow/confirm_output.rs @@ -450,5 +450,5 @@ fn new_confirm_output_obj(_args: &[Obj], kwargs: &Map) -> Result Result Result { content_remaining_shares, )? }; - Ok(LayoutObj::new(res)?.into()) + Ok(LayoutObj::new_root(res)?.into()) } diff --git a/core/embed/rust/src/ui/model_mercury/flow/get_address.rs b/core/embed/rust/src/ui/model_mercury/flow/get_address.rs index eda7695ae49..35929b5cb65 100644 --- a/core/embed/rust/src/ui/model_mercury/flow/get_address.rs +++ b/core/embed/rust/src/ui/model_mercury/flow/get_address.rs @@ -231,6 +231,6 @@ impl GetAddress { .with_page(&GetAddress::AccountInfo, content_account)? .with_page(&GetAddress::Cancel, content_cancel_info)? .with_page(&GetAddress::CancelTap, content_cancel_tap)?; - Ok(LayoutObj::new(res)?.into()) + Ok(LayoutObj::new_root(res)?.into()) } } diff --git a/core/embed/rust/src/ui/model_mercury/flow/prompt_backup.rs b/core/embed/rust/src/ui/model_mercury/flow/prompt_backup.rs index 4ffc88ac45a..b8e29768a2b 100644 --- a/core/embed/rust/src/ui/model_mercury/flow/prompt_backup.rs +++ b/core/embed/rust/src/ui/model_mercury/flow/prompt_backup.rs @@ -141,6 +141,6 @@ impl PromptBackup { .with_page(&PromptBackup::Menu, content_menu)? .with_page(&PromptBackup::SkipBackupIntro, content_skip_intro)? .with_page(&PromptBackup::SkipBackupConfirm, content_skip_confirm)?; - Ok(LayoutObj::new(res)?.into()) + Ok(LayoutObj::new_root(res)?.into()) } } diff --git a/core/embed/rust/src/ui/model_mercury/flow/request_number.rs b/core/embed/rust/src/ui/model_mercury/flow/request_number.rs index b12b3dd93af..d3b7b675331 100644 --- a/core/embed/rust/src/ui/model_mercury/flow/request_number.rs +++ b/core/embed/rust/src/ui/model_mercury/flow/request_number.rs @@ -132,6 +132,6 @@ impl RequestNumber { .with_page(&RequestNumber::Number, content_number_input)? .with_page(&RequestNumber::Menu, content_menu)? .with_page(&RequestNumber::Info, content_info)?; - Ok(LayoutObj::new(res)?.into()) + Ok(LayoutObj::new_root(res)?.into()) } } diff --git a/core/embed/rust/src/ui/model_mercury/flow/request_passphrase.rs b/core/embed/rust/src/ui/model_mercury/flow/request_passphrase.rs index 7a01f0a0e3e..3e81f855d3d 100644 --- a/core/embed/rust/src/ui/model_mercury/flow/request_passphrase.rs +++ b/core/embed/rust/src/ui/model_mercury/flow/request_passphrase.rs @@ -81,6 +81,6 @@ impl RequestPassphrase { let res = SwipeFlow::new(&RequestPassphrase::Keypad)? .with_page(&RequestPassphrase::Keypad, content_keypad)? .with_page(&RequestPassphrase::ConfirmEmpty, content_confirm_empty)?; - Ok(LayoutObj::new(res)?.into()) + Ok(LayoutObj::new_root(res)?.into()) } } diff --git a/core/embed/rust/src/ui/model_mercury/flow/set_brightness.rs b/core/embed/rust/src/ui/model_mercury/flow/set_brightness.rs index c194dcd95e4..f3a3b48249d 100644 --- a/core/embed/rust/src/ui/model_mercury/flow/set_brightness.rs +++ b/core/embed/rust/src/ui/model_mercury/flow/set_brightness.rs @@ -139,6 +139,6 @@ impl SetBrightness { .with_page(&SetBrightness::Confirm, content_confirm)? .with_page(&SetBrightness::Confirmed, content_confirmed)?; - Ok(LayoutObj::new(res)?.into()) + Ok(LayoutObj::new_root(res)?.into()) } } diff --git a/core/embed/rust/src/ui/model_mercury/flow/show_share_words.rs b/core/embed/rust/src/ui/model_mercury/flow/show_share_words.rs index e23010c38df..948c567a3f1 100644 --- a/core/embed/rust/src/ui/model_mercury/flow/show_share_words.rs +++ b/core/embed/rust/src/ui/model_mercury/flow/show_share_words.rs @@ -160,6 +160,6 @@ impl ShowShareWords { &ShowShareWords::CheckBackupIntro, content_check_backup_intro, )?; - Ok(LayoutObj::new(res)?.into()) + Ok(LayoutObj::new_root(res)?.into()) } } diff --git a/core/embed/rust/src/ui/model_mercury/flow/show_tutorial.rs b/core/embed/rust/src/ui/model_mercury/flow/show_tutorial.rs index 8e9c9f722af..9b52c181e46 100644 --- a/core/embed/rust/src/ui/model_mercury/flow/show_tutorial.rs +++ b/core/embed/rust/src/ui/model_mercury/flow/show_tutorial.rs @@ -202,6 +202,6 @@ impl ShowTutorial { .with_page(&ShowTutorial::Menu, content_menu)? .with_page(&ShowTutorial::DidYouKnow, content_did_you_know)? .with_page(&ShowTutorial::HoldToExit, content_hold_to_exit)?; - Ok(LayoutObj::new(res)?.into()) + Ok(LayoutObj::new_root(res)?.into()) } } diff --git a/core/embed/rust/src/ui/model_mercury/flow/warning_hi_prio.rs b/core/embed/rust/src/ui/model_mercury/flow/warning_hi_prio.rs index fd357c0748a..3b776b629bd 100644 --- a/core/embed/rust/src/ui/model_mercury/flow/warning_hi_prio.rs +++ b/core/embed/rust/src/ui/model_mercury/flow/warning_hi_prio.rs @@ -117,6 +117,6 @@ impl WarningHiPrio { .with_page(&WarningHiPrio::Message, content_message)? .with_page(&WarningHiPrio::Menu, content_menu)? .with_page(&WarningHiPrio::Cancelled, content_cancelled)?; - Ok(LayoutObj::new(res)?.into()) + Ok(LayoutObj::new_root(res)?.into()) } } diff --git a/core/embed/rust/src/ui/model_mercury/layout.rs b/core/embed/rust/src/ui/model_mercury/layout.rs index 07fa72dc757..5bd503500c4 100644 --- a/core/embed/rust/src/ui/model_mercury/layout.rs +++ b/core/embed/rust/src/ui/model_mercury/layout.rs @@ -43,6 +43,7 @@ use crate::{ flow::Swipable, geometry::{self, Direction}, layout::{ + base::LAYOUT_STATE, obj::{ComponentMsgObj, LayoutObj, ATTACH_TYPE_OBJ}, result::{CANCELLED, CONFIRMED, INFO}, util::{upy_disable_animation, ConfirmBlob, PropsList, RecoveryType}, @@ -1128,82 +1129,6 @@ extern "C" fn new_confirm_fido(n_args: usize, args: *const Obj, kwargs: *mut Map #[no_mangle] pub static mp_module_trezorui2: Module = obj_module! { /// from trezor import utils - /// - /// T = TypeVar("T") - /// - /// class LayoutObj(Generic[T]): - /// """Representation of a Rust-based layout object. - /// see `trezor::ui::layout::obj::LayoutObj`. - /// """ - /// - /// def attach_timer_fn(self, fn: Callable[[int, int], None], attach_type: AttachType | None) -> None: - /// """Attach a timer setter function. - /// - /// The layout object can call the timer setter with two arguments, - /// `token` and `duration_ms`. When `duration_ms` is reached, the layout object - /// expects a callback to `self.timer(token)`. - /// """ - /// - /// if utils.USE_TOUCH: - /// def touch_event(self, event: int, x: int, y: int) -> T | None: - /// """Receive a touch event `event` at coordinates `x`, `y`.""" - /// - /// if utils.USE_BUTTON: - /// def button_event(self, event: int, button: int) -> T | None: - /// """Receive a button event `event` for button `button`.""" - /// - /// def progress_event(self, value: int, description: str) -> T | None: - /// """Receive a progress event.""" - /// - /// def usb_event(self, connected: bool) -> T | None: - /// """Receive a USB connect/disconnect event.""" - /// - /// def timer(self, token: int) -> T | None: - /// """Callback for the timer set by `attach_timer_fn`. - /// - /// This function should be called by the executor after the corresponding - /// duration has expired. - /// """ - /// - /// def paint(self) -> bool: - /// """Paint the layout object on screen. - /// - /// Will only paint updated parts of the layout as required. - /// Returns True if any painting actually happened. - /// """ - /// - /// def request_complete_repaint(self) -> None: - /// """Request a complete repaint of the screen. - /// - /// Does not repaint the screen, a subsequent call to `paint()` is required. - /// """ - /// - /// if __debug__: - /// def trace(self, tracer: Callable[[str], None]) -> None: - /// """Generate a JSON trace of the layout object. - /// - /// The JSON can be emitted as a sequence of calls to `tracer`, each of - /// which is not necessarily a valid JSON chunk. The caller must - /// reassemble the chunks to get a sensible result. - /// """ - /// - /// def bounds(self) -> None: - /// """Paint bounds of individual components on screen.""" - /// - /// def page_count(self) -> int: - /// """Return the number of pages in the layout object.""" - /// - /// def get_transition_out(self) -> AttachType: - /// """Return the transition type.""" - /// - /// def __del__(self) -> None: - /// """Calls drop on contents of the root component.""" - /// - /// class UiResult: - /// """Result of a UI operation.""" - /// pass - /// - /// mock:global Qstr::MP_QSTR___name__ => Qstr::MP_QSTR_trezorui2.to_obj(), /// CONFIRMED: UiResult @@ -1731,4 +1656,11 @@ pub static mp_module_trezorui2: Module = obj_module! { /// SWIPE_RIGHT: ClassVar[int] Qstr::MP_QSTR_AttachType => ATTACH_TYPE_OBJ.as_obj(), + /// class LayoutState: + /// """Layout state.""" + /// INITIAL: "ClassVar[LayoutState]" + /// ATTACHED: "ClassVar[LayoutState]" + /// TRANSITIONING: "ClassVar[LayoutState]" + /// DONE: "ClassVar[LayoutState]" + Qstr::MP_QSTR_LayoutState => LAYOUT_STATE.as_obj(), }; diff --git a/core/embed/rust/src/ui/model_tr/component/button_controller.rs b/core/embed/rust/src/ui/model_tr/component/button_controller.rs index 28093887900..261d81f0617 100644 --- a/core/embed/rust/src/ui/model_tr/component/button_controller.rs +++ b/core/embed/rust/src/ui/model_tr/component/button_controller.rs @@ -4,7 +4,7 @@ use super::{ use crate::{ time::{Duration, Instant}, ui::{ - component::{base::Event, Component, EventCtx, Pad, TimerToken}, + component::{base::Event, Component, EventCtx, Pad, Timer}, event::{ButtonEvent, PhysicalButton}, geometry::Rect, shape::Renderer, @@ -122,7 +122,7 @@ pub struct ButtonContainer { /// `ButtonControllerMsg::Triggered` long_press_ms: u32, /// Timer for sending `ButtonControllerMsg::LongPressed` - long_pressed_timer: Option, + long_pressed_timer: Timer, /// Whether it should even send `ButtonControllerMsg::LongPressed` events /// (optional) send_long_press: bool, @@ -141,7 +141,7 @@ impl ButtonContainer { button_type: ButtonType::from_button_details(pos, btn_details), pressed_since: None, long_press_ms: DEFAULT_LONG_PRESS_MS, - long_pressed_timer: None, + long_pressed_timer: Timer::new(), send_long_press, } } @@ -190,7 +190,7 @@ impl ButtonContainer { Instant::now().saturating_duration_since(since).to_millis() > self.long_press_ms }); self.pressed_since = None; - self.long_pressed_timer = None; + self.long_pressed_timer.stop(); Some(ButtonControllerMsg::Triggered(self.pos, long_press)) } ButtonType::HoldToConfirm(_) => { @@ -216,20 +216,20 @@ impl ButtonContainer { pub fn got_pressed(&mut self, ctx: &mut EventCtx) { self.pressed_since = Some(Instant::now()); if self.send_long_press { - self.long_pressed_timer = - Some(ctx.request_timer(Duration::from_millis(self.long_press_ms))); + self.long_pressed_timer + .start(ctx, Duration::from_millis(self.long_press_ms)); } } /// Reset the pressed information. pub fn reset(&mut self) { self.pressed_since = None; - self.long_pressed_timer = None; + self.long_pressed_timer.stop(); } /// Whether token matches what we have - pub fn is_timer_token(&self, token: TimerToken) -> bool { - self.long_pressed_timer == Some(token) + pub fn timer_event(&mut self, event: Event) -> bool { + self.long_pressed_timer.expire(event) } /// Registering hold event. @@ -380,14 +380,14 @@ impl ButtonController { } } - fn handle_long_press_timer_token(&mut self, token: TimerToken) -> Option { - if self.left_btn.is_timer_token(token) { + fn handle_long_press_timers(&mut self, event: Event) -> Option { + if self.left_btn.timer_event(event) { return Some(ButtonPos::Left); } - if self.middle_btn.is_timer_token(token) { + if self.middle_btn.timer_event(event) { return Some(ButtonPos::Middle); } - if self.right_btn.is_timer_token(token) { + if self.right_btn.timer_event(event) { return Some(ButtonPos::Right); } None @@ -572,11 +572,11 @@ impl Component for ButtonController { event } // Timer - handle clickable properties and HoldToConfirm expiration - Event::Timer(token) => { + Event::Timer(_) => { if let Some(ignore_btn_delay) = &mut self.ignore_btn_delay { - ignore_btn_delay.handle_timer_token(token); + ignore_btn_delay.handle_timers(event); } - if let Some(pos) = self.handle_long_press_timer_token(token) { + if let Some(pos) = self.handle_long_press_timers(event) { return Some(ButtonControllerMsg::LongPressed(pos)); } self.handle_htc_expiration(ctx, event) @@ -624,9 +624,9 @@ struct IgnoreButtonDelay { /// Whether right button is currently clickable right_clickable: bool, /// Timer for setting the left_clickable - left_clickable_timer: Option, + left_clickable_timer: Timer, /// Timer for setting the right_clickable - right_clickable_timer: Option, + right_clickable_timer: Timer, } impl IgnoreButtonDelay { @@ -635,8 +635,8 @@ impl IgnoreButtonDelay { delay: Duration::from_millis(delay_ms), left_clickable: true, right_clickable: true, - left_clickable_timer: None, - right_clickable_timer: None, + left_clickable_timer: Timer::new(), + right_clickable_timer: Timer::new(), } } @@ -644,11 +644,11 @@ impl IgnoreButtonDelay { match pos { ButtonPos::Left => { self.left_clickable = true; - self.left_clickable_timer = None; + self.left_clickable_timer.stop(); } ButtonPos::Right => { self.right_clickable = true; - self.right_clickable_timer = None; + self.right_clickable_timer.stop(); } ButtonPos::Middle => {} } @@ -656,10 +656,10 @@ impl IgnoreButtonDelay { pub fn handle_button_press(&mut self, ctx: &mut EventCtx, button: PhysicalButton) { if matches!(button, PhysicalButton::Left) { - self.right_clickable_timer = Some(ctx.request_timer(self.delay)); + self.right_clickable_timer.start(ctx, self.delay); } if matches!(button, PhysicalButton::Right) { - self.left_clickable_timer = Some(ctx.request_timer(self.delay)); + self.left_clickable_timer.start(ctx, self.delay); } } @@ -673,22 +673,20 @@ impl IgnoreButtonDelay { false } - pub fn handle_timer_token(&mut self, token: TimerToken) { - if self.left_clickable_timer == Some(token) { + pub fn handle_timers(&mut self, event: Event) { + if self.left_clickable_timer.expire(event) { self.left_clickable = false; - self.left_clickable_timer = None; } - if self.right_clickable_timer == Some(token) { + if self.right_clickable_timer.expire(event) { self.right_clickable = false; - self.right_clickable_timer = None; } } pub fn reset(&mut self) { self.left_clickable = true; self.right_clickable = true; - self.left_clickable_timer = None; - self.right_clickable_timer = None; + self.left_clickable_timer.stop(); + self.right_clickable_timer.stop(); } } @@ -700,7 +698,7 @@ impl IgnoreButtonDelay { /// Can be started e.g. by holding left/right button. pub struct AutomaticMover { /// For requesting timer events repeatedly - timer_token: Option, + timer: Timer, /// Which direction should we go (which button is down) moving_direction: Option, /// How many screens were moved automatically @@ -721,7 +719,7 @@ impl AutomaticMover { } Self { - timer_token: None, + timer: Timer::new(), moving_direction: None, auto_moved_screens: 0, duration_func: default_duration_func, @@ -760,12 +758,12 @@ impl AutomaticMover { pub fn start_moving(&mut self, ctx: &mut EventCtx, button: ButtonPos) { self.auto_moved_screens = 0; self.moving_direction = Some(button); - self.timer_token = Some(ctx.request_timer(self.get_auto_move_duration())); + self.timer.start(ctx, self.get_auto_move_duration()); } pub fn stop_moving(&mut self) { self.moving_direction = None; - self.timer_token = None; + self.timer.stop(); } } @@ -781,15 +779,11 @@ impl Component for AutomaticMover { fn render<'s>(&'s self, _target: &mut impl Renderer<'s>) {} fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { - // Moving automatically only when we receive a TimerToken that we have - // requested before - if let Event::Timer(token) = event { - if self.timer_token == Some(token) && self.moving_direction.is_some() { - // Request new token and send the appropriate button trigger event - self.timer_token = Some(ctx.request_timer(self.get_auto_move_duration())); - self.auto_moved_screens += 1; - return self.moving_direction; - } + if self.timer.expire(event) && self.moving_direction.is_some() { + // Restart timer and send the appropriate button trigger event + self.timer.start(ctx, self.get_auto_move_duration()); + self.auto_moved_screens += 1; + return self.moving_direction; } None } diff --git a/core/embed/rust/src/ui/model_tr/component/changing_text.rs b/core/embed/rust/src/ui/model_tr/component/changing_text.rs index 7aa6c5ac945..9546fd311a7 100644 --- a/core/embed/rust/src/ui/model_tr/component/changing_text.rs +++ b/core/embed/rust/src/ui/model_tr/component/changing_text.rs @@ -177,7 +177,7 @@ impl ChangingTextLine { let x_offset = if self.text.len() % 2 == 0 { 0 } else { 2 }; let baseline = Point::new(self.pad.area.x0 + x_offset, self.y_baseline()); - shape::Text::new(baseline, self.text.as_ref()) + shape::Text::new(baseline, &text_to_display) .with_font(self.font) .render(target); } diff --git a/core/embed/rust/src/ui/model_tr/component/progress.rs b/core/embed/rust/src/ui/model_tr/component/progress.rs index b24109d1d92..045078f3368 100644 --- a/core/embed/rust/src/ui/model_tr/component/progress.rs +++ b/core/embed/rust/src/ui/model_tr/component/progress.rs @@ -127,6 +127,9 @@ impl Component for Progress { self.description_pad.clear(); } }); + } else { + self.title.event(ctx, event); + self.description.event(ctx, event); } None } diff --git a/core/embed/rust/src/ui/model_tr/component/show_more.rs b/core/embed/rust/src/ui/model_tr/component/show_more.rs index c2a10e1b555..8f8dae0f656 100644 --- a/core/embed/rust/src/ui/model_tr/component/show_more.rs +++ b/core/embed/rust/src/ui/model_tr/component/show_more.rs @@ -1,7 +1,7 @@ use crate::{ strutil::TString, ui::{ - component::{Child, Component, Event, EventCtx}, + component::{Child, Component, Event, EventCtx, Never}, geometry::{Insets, Rect}, shape::Renderer, }, @@ -43,7 +43,7 @@ where impl Component for ShowMore where - T: Component, + T: Component, { type Msg = CancelInfoConfirmMsg; @@ -56,6 +56,7 @@ where } fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + self.content.event(ctx, event); let button_event = self.buttons.event(ctx, event); if let Some(ButtonControllerMsg::Triggered(pos, _)) = button_event { diff --git a/core/embed/rust/src/ui/model_tr/layout.rs b/core/embed/rust/src/ui/model_tr/layout.rs index f957ca85100..a85d1823410 100644 --- a/core/embed/rust/src/ui/model_tr/layout.rs +++ b/core/embed/rust/src/ui/model_tr/layout.rs @@ -43,10 +43,11 @@ use crate::{ }, TextStyle, }, - ComponentExt, FormattedText, Label, LineBreaking, Timeout, + ComponentExt, FormattedText, Label, LineBreaking, Never, Timeout, }, geometry, layout::{ + base::LAYOUT_STATE, obj::{ComponentMsgObj, LayoutObj, ATTACH_TYPE_OBJ}, result::{CANCELLED, CONFIRMED, INFO}, util::{upy_disable_animation, ConfirmBlob, RecoveryType}, @@ -66,7 +67,7 @@ impl From for Obj { impl ComponentMsgObj for ShowMore where - T: Component, + T: Component, { fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { match msg { @@ -1402,11 +1403,11 @@ extern "C" fn new_confirm_recovery(n_args: usize, args: *const Obj, kwargs: *mut let description: TString = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?; let button: TString<'static> = kwargs.get(Qstr::MP_QSTR_button)?.try_into()?; let recovery_type: RecoveryType = kwargs.get(Qstr::MP_QSTR_recovery_type)?.try_into()?; - let show_info: bool = kwargs.get(Qstr::MP_QSTR_show_info)?.try_into()?; + let show_instructions: bool = kwargs.get(Qstr::MP_QSTR_show_instructions)?.try_into()?; let mut paragraphs = ParagraphVecShort::new(); paragraphs.add(Paragraph::new(&theme::TEXT_NORMAL, description)); - if show_info { + if show_instructions { paragraphs .add(Paragraph::new( &theme::TEXT_NORMAL, @@ -1981,7 +1982,7 @@ pub static mp_module_trezorui2: Module = obj_module! { /// button: str, /// recovery_type: RecoveryType, /// info_button: bool, # unused on TR - /// show_info: bool, + /// show_instructions: bool, /// ) -> LayoutObj[UiResult]: /// """Device recovery homescreen.""" Qstr::MP_QSTR_confirm_recovery => obj_fn_kw!(0, new_confirm_recovery).as_obj(), @@ -2075,4 +2076,12 @@ pub static mp_module_trezorui2: Module = obj_module! { /// SWIPE_LEFT: ClassVar[int] /// SWIPE_RIGHT: ClassVar[int] Qstr::MP_QSTR_AttachType => ATTACH_TYPE_OBJ.as_obj(), + + /// class LayoutState: + /// """Layout state.""" + /// INITIAL: "ClassVar[LayoutState]" + /// ATTACHED: "ClassVar[LayoutState]" + /// TRANSITIONING: "ClassVar[LayoutState]" + /// DONE: "ClassVar[LayoutState]" + Qstr::MP_QSTR_LayoutState => LAYOUT_STATE.as_obj(), }; diff --git a/core/embed/rust/src/ui/model_tt/bootloader/mod.rs b/core/embed/rust/src/ui/model_tt/bootloader/mod.rs index f8e53c53e9e..e83c6f93696 100644 --- a/core/embed/rust/src/ui/model_tt/bootloader/mod.rs +++ b/core/embed/rust/src/ui/model_tt/bootloader/mod.rs @@ -36,7 +36,6 @@ use super::theme::BLACK; #[cfg(feature = "new_rendering")] use crate::ui::{ - constant, display::toif::Toif, geometry::{Alignment, Alignment2D, Offset}, shape, diff --git a/core/embed/rust/src/ui/model_tt/component/bl_confirm.rs b/core/embed/rust/src/ui/model_tt/component/bl_confirm.rs index ab268d1d94b..30526f3b663 100644 --- a/core/embed/rust/src/ui/model_tt/component/bl_confirm.rs +++ b/core/embed/rust/src/ui/model_tt/component/bl_confirm.rs @@ -32,6 +32,7 @@ const CONTENT_AREA: Rect = Rect::new( ); #[derive(Copy, Clone, ToPrimitive)] +#[cfg_attr(feature = "debug", derive(ufmt::derive::uDebug))] pub enum ConfirmMsg { Cancel = 1, Confirm = 2, diff --git a/core/embed/rust/src/ui/model_tt/component/button.rs b/core/embed/rust/src/ui/model_tt/component/button.rs index 6bc0ac3e818..16250558982 100644 --- a/core/embed/rust/src/ui/model_tt/component/button.rs +++ b/core/embed/rust/src/ui/model_tt/component/button.rs @@ -5,18 +5,18 @@ use crate::{ time::Duration, ui::{ component::{ - Component, ComponentExt, Event, EventCtx, FixedHeightBar, MsgMap, Split, TimerToken, + Component, ComponentExt, Event, EventCtx, FixedHeightBar, MsgMap, Split, Timer, }, display::{self, toif::Icon, Color, Font}, event::TouchEvent, geometry::{Alignment2D, Insets, Offset, Point, Rect}, - shape, - shape::Renderer, + shape::{self, Renderer}, }, }; use super::theme; +#[cfg_attr(feature = "debug", derive(ufmt::derive::uDebug))] pub enum ButtonMsg { Pressed, Released, @@ -31,7 +31,7 @@ pub struct Button { styles: ButtonStyleSheet, state: State, long_press: Option, - long_timer: Option, + long_timer: Timer, haptics: bool, } @@ -48,7 +48,7 @@ impl Button { styles: theme::button_default(), state: State::Initial, long_press: None, - long_timer: None, + long_timer: Timer::new(), haptics: true, } } @@ -317,7 +317,7 @@ impl Component for Button { } self.set(ctx, State::Pressed); if let Some(duration) = self.long_press { - self.long_timer = Some(ctx.request_timer(duration)); + self.long_timer.start(ctx, duration) } return Some(ButtonMsg::Pressed); } @@ -349,21 +349,18 @@ impl Component for Button { _ => { // Touch finished outside our area. self.set(ctx, State::Initial); - self.long_timer = None; + self.long_timer.stop(); } } } - Event::Timer(token) => { - if self.long_timer == Some(token) { - self.long_timer = None; - if matches!(self.state, State::Pressed) { - #[cfg(feature = "haptic")] - if self.haptics { - haptic::play(HapticEffect::ButtonPress); - } - self.set(ctx, State::Initial); - return Some(ButtonMsg::LongPressed); + Event::Timer(_) if self.long_timer.expire(event) => { + if matches!(self.state, State::Pressed) { + #[cfg(feature = "haptic")] + if self.haptics { + haptic::play(HapticEffect::ButtonPress); } + self.set(ctx, State::Initial); + return Some(ButtonMsg::LongPressed); } } _ => {} @@ -555,6 +552,7 @@ impl Button { } } +#[cfg_attr(feature = "debug", derive(ufmt::derive::uDebug))] pub enum CancelConfirmMsg { Cancelled, Confirmed, @@ -566,12 +564,14 @@ type CancelInfoConfirm = type CancelConfirm = FixedHeightBar, MsgMap>>; #[derive(Clone, Copy)] +#[cfg_attr(feature = "debug", derive(ufmt::derive::uDebug))] pub enum CancelInfoConfirmMsg { Cancelled, Info, Confirmed, } +#[cfg_attr(feature = "debug", derive(ufmt::derive::uDebug))] pub enum SelectWordMsg { Selected(usize), } diff --git a/core/embed/rust/src/ui/model_tt/component/dialog.rs b/core/embed/rust/src/ui/model_tt/component/dialog.rs index c33b3cef2f4..23679a49213 100644 --- a/core/embed/rust/src/ui/model_tt/component/dialog.rs +++ b/core/embed/rust/src/ui/model_tt/component/dialog.rs @@ -16,6 +16,7 @@ use crate::{ use super::theme; +#[cfg_attr(feature = "debug", derive(ufmt::derive::uDebug))] pub enum DialogMsg { Content(T), Controls(U), diff --git a/core/embed/rust/src/ui/model_tt/component/fido.rs b/core/embed/rust/src/ui/model_tt/component/fido.rs index c83def9b65f..2cb10950dc3 100644 --- a/core/embed/rust/src/ui/model_tt/component/fido.rs +++ b/core/embed/rust/src/ui/model_tt/component/fido.rs @@ -23,6 +23,7 @@ const SCROLLBAR_HEIGHT: i16 = 10; const APP_NAME_PADDING: i16 = 12; const APP_NAME_HEIGHT: i16 = 30; +#[cfg_attr(feature = "debug", derive(ufmt::derive::uDebug))] pub enum FidoMsg { Confirmed(usize), Cancelled, diff --git a/core/embed/rust/src/ui/model_tt/component/frame.rs b/core/embed/rust/src/ui/model_tt/component/frame.rs index bbfa77dd00d..d989204e61d 100644 --- a/core/embed/rust/src/ui/model_tt/component/frame.rs +++ b/core/embed/rust/src/ui/model_tt/component/frame.rs @@ -21,6 +21,7 @@ pub struct Frame { content: Child, } +#[cfg_attr(feature = "debug", derive(ufmt::derive::uDebug))] pub enum FrameMsg { Content(T), Button(CancelInfoConfirmMsg), diff --git a/core/embed/rust/src/ui/model_tt/component/homescreen/mod.rs b/core/embed/rust/src/ui/model_tt/component/homescreen/mod.rs index e8c70f7b0b5..4fd8f6f639c 100644 --- a/core/embed/rust/src/ui/model_tt/component/homescreen/mod.rs +++ b/core/embed/rust/src/ui/model_tt/component/homescreen/mod.rs @@ -7,7 +7,7 @@ use crate::{ translations::TR, trezorhal::usb::usb_configured, ui::{ - component::{Component, Event, EventCtx, Pad, TimerToken}, + component::{Component, Event, EventCtx, Pad, Timer}, display::{ self, image::{ImageInfo, ToifFormat}, @@ -58,9 +58,10 @@ pub struct Homescreen { loader: Loader, pad: Pad, paint_notification_only: bool, - delay: Option, + delay: Timer, } +#[cfg_attr(feature = "debug", derive(ufmt::derive::uDebug))] pub enum HomescreenMsg { Dismissed, } @@ -79,7 +80,7 @@ impl Homescreen { loader: Loader::with_lock_icon().with_durations(LOADER_DURATION, LOADER_DURATION / 3), pad: Pad::with_background(theme::BG), paint_notification_only: false, - delay: None, + delay: Timer::new(), } } @@ -152,11 +153,11 @@ impl Homescreen { if self.loader.is_animating() { self.loader.start_growing(ctx, Instant::now()); } else { - self.delay = Some(ctx.request_timer(LOADER_DELAY)); + self.delay.start(ctx, LOADER_DELAY); } } Event::Touch(TouchEvent::TouchEnd(_)) => { - self.delay = None; + self.delay.stop(); let now = Instant::now(); if self.loader.is_completely_grown(now) { return true; @@ -165,8 +166,7 @@ impl Homescreen { self.loader.start_shrinking(ctx, now); } } - Event::Timer(token) if Some(token) == self.delay => { - self.delay = None; + Event::Timer(_) if self.delay.expire(event) => { self.pad.clear(); self.paint_notification_only = false; self.loader.start_growing(ctx, Instant::now()); diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/bip39.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/bip39.rs index c63c3fde583..906406303cb 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/bip39.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/bip39.rs @@ -93,7 +93,7 @@ impl Component for Bip39Input { fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { self.button_suggestion.event(ctx, event); - if self.multi_tap.is_timeout_event(event) { + if self.multi_tap.timeout_event(event) { self.on_timeout(ctx) } else if let Some(ButtonMsg::Clicked) = self.button.event(ctx, event) { self.on_input_click(ctx) diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/common.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/common.rs index d4dcb7b7870..fe2d558589b 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/common.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/common.rs @@ -1,7 +1,7 @@ use crate::{ time::Duration, ui::{ - component::{text::common::TextEdit, Event, EventCtx, TimerToken}, + component::{text::common::TextEdit, Event, EventCtx, Timer}, display::{self, Color, Font}, geometry::{Offset, Point, Rect}, shape, @@ -24,7 +24,16 @@ struct Pending { /// one). press: usize, /// Timer for clearing the pending state. - timer: TimerToken, + timer: Timer, +} + +impl Pending { + /// Create a new pending state for a key. + fn start(ctx: &mut EventCtx, key: usize, press: usize, timeout: Duration) -> Self { + let mut timer = Timer::new(); + timer.start(ctx, timeout); + Self { key, press, timer } + } } impl MultiTapKeyboard { @@ -46,15 +55,12 @@ impl MultiTapKeyboard { self.pending.as_ref().map(|p| p.press) } - /// Return the token for the currently pending timer. - pub fn pending_timer(&self) -> Option { - self.pending.as_ref().map(|p| p.timer) - } - /// Returns `true` if `event` is an `Event::Timer` for the currently pending /// timer. - pub fn is_timeout_event(&self, event: Event) -> bool { - matches!((event, self.pending_timer()), (Event::Timer(t), Some(pt)) if pt == t) + pub fn timeout_event(&mut self, event: Event) -> bool { + self.pending + .as_mut() + .map_or(false, |t| t.timer.expire(event)) } /// Reset to the empty state. Takes `EventCtx` to request a paint pass (to @@ -95,11 +101,7 @@ impl MultiTapKeyboard { // transition only happens as a result of an append op, so the painting should // be requested by handling the `TextEdit`. self.pending = if key_text.len() > 1 { - Some(Pending { - key, - press, - timer: ctx.request_timer(self.timeout), - }) + Some(Pending::start(ctx, key, press, self.timeout)) } else { None }; diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/mnemonic.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/mnemonic.rs index 9899f3fed36..94bbe5d7d19 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/mnemonic.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/mnemonic.rs @@ -13,6 +13,7 @@ use crate::{ pub const MNEMONIC_KEY_COUNT: usize = 9; +#[cfg_attr(feature = "debug", derive(ufmt::derive::uDebug))] pub enum MnemonicKeyboardMsg { Confirmed, Previous, diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs index 1b10fa6216d..d1795c0b3c1 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs @@ -20,6 +20,7 @@ use crate::{ use core::cell::Cell; +#[cfg_attr(feature = "debug", derive(ufmt::derive::uDebug))] pub enum PassphraseKeyboardMsg { Confirmed, Cancelled, @@ -221,9 +222,15 @@ impl Component for PassphraseKeyboard { } fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { - if self.input.inner().multi_tap.is_timeout_event(event) { - self.input - .mutate(ctx, |ctx, i| i.multi_tap.clear_pending_state(ctx)); + let multitap_timeout = self.input.mutate(ctx, |ctx, i| { + if i.multi_tap.timeout_event(event) { + i.multi_tap.clear_pending_state(ctx); + true + } else { + false + } + }); + if multitap_timeout { return None; } if let Some(swipe) = self.page_swipe.event(ctx, event) { diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/pin.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/pin.rs index 3417b60eae1..3cd478ee25d 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/pin.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/pin.rs @@ -7,7 +7,7 @@ use crate::{ ui::{ component::{ base::ComponentExt, text::TextStyle, Child, Component, Event, EventCtx, Label, Maybe, - Never, Pad, TimerToken, + Never, Pad, Timer, }, display::{self, Font}, event::TouchEvent, @@ -23,6 +23,7 @@ use crate::{ }, }; +#[cfg_attr(feature = "debug", derive(ufmt::derive::uDebug))] pub enum PinKeyboardMsg { Confirmed, Cancelled, @@ -54,7 +55,7 @@ pub struct PinKeyboard<'a> { cancel_btn: Child>, confirm_btn: Child