From 2703d714c2dc1f3ba907d223825a58e2a914b173 Mon Sep 17 00:00:00 2001 From: Jan Pochyla Date: Tue, 8 Jun 2021 13:29:03 +0200 Subject: [PATCH] feat(core): add Rust UI components, layouts, text rendering [no changelog] --- core/Makefile | 2 +- core/SConscript.firmware | 9 + core/SConscript.unix | 10 + .../modtrezorutils/modtrezorutils-meminfo.h | 17 + core/embed/extmod/rustmods/modtrezorui2.c | 49 ++ core/embed/firmware/mpconfigport.h | 1 + core/embed/rust/Cargo.lock | 107 ++- core/embed/rust/Cargo.toml | 12 +- core/embed/rust/librust.h | 6 + core/embed/rust/librust_qstr.h | 10 + core/embed/rust/src/error.rs | 12 +- core/embed/rust/src/lib.rs | 10 +- core/embed/rust/src/micropython/buffer.rs | 18 + core/embed/rust/src/micropython/gc.rs | 28 +- core/embed/rust/src/protobuf/defs.rs | 8 +- core/embed/rust/src/protobuf/obj.rs | 16 +- core/embed/rust/src/trace.rs | 38 + core/embed/rust/src/trezorhal/display.rs | 61 ++ core/embed/rust/src/ui/component/base.rs | 151 ++++ core/embed/rust/src/ui/component/mod.rs | 5 + .../rust/src/ui/component/model_t1/mod.rs | 1 + .../rust/src/ui/component/model_tt/button.rs | 224 ++++++ .../rust/src/ui/component/model_tt/dialog.rs | 80 +++ .../rust/src/ui/component/model_tt/empty.rs | 13 + .../rust/src/ui/component/model_tt/label.rs | 90 +++ .../rust/src/ui/component/model_tt/mod.rs | 17 + .../rust/src/ui/component/model_tt/page.rs | 136 ++++ .../src/ui/component/model_tt/passphrase.rs | 319 +++++++++ .../rust/src/ui/component/model_tt/pin.rs | 256 +++++++ .../rust/src/ui/component/model_tt/swipe.rs | 149 ++++ .../rust/src/ui/component/model_tt/text.rs | 647 ++++++++++++++++++ .../rust/src/ui/component/model_tt/theme.rs | 91 +++ core/embed/rust/src/ui/display.rs | 156 +++++ core/embed/rust/src/ui/geometry.rs | 229 +++++++ core/embed/rust/src/ui/layout/example.rs | 117 ++++ core/embed/rust/src/ui/layout/mod.rs | 2 + core/embed/rust/src/ui/layout/obj.rs | 357 ++++++++++ core/embed/rust/src/ui/macros.rs | 9 + core/embed/rust/src/ui/mod.rs | 7 + core/embed/unix/mpconfigport.h | 1 + core/mocks/generated/trezorui2.pyi | 6 + core/src/trezor/ui/__init__.py | 25 + core/tools/analyze-memory-dump.py | 2 + 43 files changed, 3457 insertions(+), 47 deletions(-) create mode 100644 core/embed/extmod/rustmods/modtrezorui2.c create mode 100644 core/embed/rust/src/trace.rs create mode 100644 core/embed/rust/src/ui/component/base.rs create mode 100644 core/embed/rust/src/ui/component/mod.rs create mode 100644 core/embed/rust/src/ui/component/model_t1/mod.rs create mode 100644 core/embed/rust/src/ui/component/model_tt/button.rs create mode 100644 core/embed/rust/src/ui/component/model_tt/dialog.rs create mode 100644 core/embed/rust/src/ui/component/model_tt/empty.rs create mode 100644 core/embed/rust/src/ui/component/model_tt/label.rs create mode 100644 core/embed/rust/src/ui/component/model_tt/mod.rs create mode 100644 core/embed/rust/src/ui/component/model_tt/page.rs create mode 100644 core/embed/rust/src/ui/component/model_tt/passphrase.rs create mode 100644 core/embed/rust/src/ui/component/model_tt/pin.rs create mode 100644 core/embed/rust/src/ui/component/model_tt/swipe.rs create mode 100644 core/embed/rust/src/ui/component/model_tt/text.rs create mode 100644 core/embed/rust/src/ui/component/model_tt/theme.rs create mode 100644 core/embed/rust/src/ui/display.rs create mode 100644 core/embed/rust/src/ui/geometry.rs create mode 100644 core/embed/rust/src/ui/layout/example.rs create mode 100644 core/embed/rust/src/ui/layout/mod.rs create mode 100644 core/embed/rust/src/ui/layout/obj.rs create mode 100644 core/embed/rust/src/ui/macros.rs create mode 100644 core/embed/rust/src/ui/mod.rs create mode 100644 core/mocks/generated/trezorui2.pyi diff --git a/core/Makefile b/core/Makefile index ad269aa4d..92efe8134 100644 --- a/core/Makefile +++ b/core/Makefile @@ -79,7 +79,7 @@ test: ## run unit tests cd tests ; ./run_tests.sh $(TESTOPTS) test_rust: ## run rs unit tests - cd embed/rust ; cargo test --features test -- --test-threads=1 + cd embed/rust ; cargo test --features test,ui,ui_debug -- --test-threads=1 test_emu: ## run selected device tests from python-trezor $(EMU_TEST) $(PYTEST) $(TESTPATH)/device_tests $(TESTOPTS) diff --git a/core/SConscript.firmware b/core/SConscript.firmware index 0acb4e619..262f26579 100644 --- a/core/SConscript.firmware +++ b/core/SConscript.firmware @@ -6,6 +6,7 @@ import os BITCOIN_ONLY = ARGUMENTS.get('BITCOIN_ONLY', '0') EVERYTHING = BITCOIN_ONLY != '1' TREZOR_MODEL = ARGUMENTS.get('TREZOR_MODEL', 'T') +NEW_UI = TREZOR_MODEL == '1' or os.environ.get('NEW_UI', '0') == '1' FEATURE_FLAGS = { "RDI": True, @@ -181,6 +182,10 @@ SOURCE_MOD += [ SOURCE_MOD += [ 'embed/extmod/rustmods/modtrezorproto.c', ] +if NEW_UI: + SOURCE_MOD += [ + 'embed/extmod/rustmods/modtrezorui2.c', + ] # modutime SOURCE_MOD += [ @@ -687,6 +692,10 @@ def cargo_build(): features = [] if BITCOIN_ONLY == "1": features.append("bitcoin_only") + if NEW_UI: + features.append("ui") + if PYOPT == "0": + features.append("ui_debug") return f'cd embed/rust; cargo build {profile} --target={RUST_TARGET} --target-dir=../../build/firmware/rust --features "{" ".join(features)}"' diff --git a/core/SConscript.unix b/core/SConscript.unix index fa2446fec..fb87b9680 100644 --- a/core/SConscript.unix +++ b/core/SConscript.unix @@ -6,6 +6,7 @@ import os BITCOIN_ONLY = ARGUMENTS.get('BITCOIN_ONLY', '0') EVERYTHING = BITCOIN_ONLY != '1' TREZOR_MODEL = ARGUMENTS.get('TREZOR_MODEL', 'T') +NEW_UI = TREZOR_MODEL == '1' or ARGUMENTS.get('NEW_UI', '0') == '1' FEATURE_FLAGS = { "SECP256K1_ZKP": False, @@ -178,6 +179,10 @@ SOURCE_MOD += [ SOURCE_MOD += [ 'embed/extmod/rustmods/modtrezorproto.c', ] +if NEW_UI: + SOURCE_MOD += [ + 'embed/extmod/rustmods/modtrezorui2.c', + ] # modutime SOURCE_MOD += [ @@ -637,6 +642,11 @@ def cargo_build(): features = [] if BITCOIN_ONLY == "1": features.append("bitcoin_only") + if PYOPT == "0": + features.append("ui") + features.append("ui_debug") + elif NEW_UI: + features.append("ui") return f'cd embed/rust; cargo build {profile} --target-dir=../../build/unix/rust --features "{" ".join(features)}"' diff --git a/core/embed/extmod/modtrezorutils/modtrezorutils-meminfo.h b/core/embed/extmod/modtrezorutils/modtrezorutils-meminfo.h index 8bb6e3fff..9488ff2ab 100644 --- a/core/embed/extmod/modtrezorutils/modtrezorutils-meminfo.h +++ b/core/embed/extmod/modtrezorutils/modtrezorutils-meminfo.h @@ -307,6 +307,12 @@ typedef struct _mp_obj_protomsg_t { mp_map_t map; } mp_obj_protomsg_t; +typedef struct _mp_obj_uilayout_t { + mp_obj_base_t base; + ssize_t _refcell_borrow_flag; + void *inner; +} mp_obj_uilayout_t; + void dump_bound_method(FILE *out, const mp_obj_bound_meth_t *meth) { print_type(out, "method", NULL, meth, false); @@ -516,6 +522,13 @@ void dump_protodef(FILE *out, const mp_obj_t *value) { fprintf(out, "},\n"); } +void dump_uilayout(FILE *out, const mp_obj_uilayout_t *value) { + print_type(out, "uilayout", NULL, value, false); + fprintf(out, ",\n\"inner\": \"%p\"},\n", value->inner); + print_type(out, "uilayoutinner", NULL, value->inner, true); + fprintf(out, ",\n"); +} + void dump_value_opt(FILE *out, mp_const_obj_t value, bool eval_short) { if (!eval_short && is_short(value)) return; @@ -640,6 +653,10 @@ void dump_value_opt(FILE *out, mp_const_obj_t value, bool eval_short) { dump_protodef(out, value); } + else if (mp_obj_is_type(value, ui_debug_layout_type())) { + dump_uilayout(out, value); + } + else { print_type(out, "unknown", NULL, value, true); fprintf(out, ",\n"); diff --git a/core/embed/extmod/rustmods/modtrezorui2.c b/core/embed/extmod/rustmods/modtrezorui2.c new file mode 100644 index 000000000..bef848c1f --- /dev/null +++ b/core/embed/extmod/rustmods/modtrezorui2.c @@ -0,0 +1,49 @@ +/* + * This file is part of the Trezor project, https://trezor.io/ + * + * Copyright (c) SatoshiLabs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "py/runtime.h" + +#if MICROPY_PY_TREZORUI2 + +#include "librust.h" + +/// def layout_new_example(text: str) -> None: +/// """Example layout.""" +STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorui2_layout_new_example_obj, + ui_layout_new_example); + +STATIC const mp_rom_map_elem_t mp_module_trezorui2_globals_table[] = { + {MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_trezorui2)}, + + {MP_ROM_QSTR(MP_QSTR_layout_new_example), + MP_ROM_PTR(&mod_trezorui2_layout_new_example_obj)}, +}; + +STATIC MP_DEFINE_CONST_DICT(mp_module_trezorui2_globals, + mp_module_trezorui2_globals_table); + +const mp_obj_module_t mp_module_trezorui2 = { + .base = {&mp_type_module}, + .globals = (mp_obj_dict_t *)&mp_module_trezorui2_globals, +}; + +MP_REGISTER_MODULE(MP_QSTR_trezorui2, mp_module_trezorui2, + MICROPY_PY_TREZORUI2); + +#endif // MICROPY_PY_TREZORUI2 diff --git a/core/embed/firmware/mpconfigport.h b/core/embed/firmware/mpconfigport.h index 8ac3bbf36..3c31749fe 100644 --- a/core/embed/firmware/mpconfigport.h +++ b/core/embed/firmware/mpconfigport.h @@ -157,6 +157,7 @@ #define MICROPY_PY_TREZORUI (1) #define MICROPY_PY_TREZORUTILS (1) #define MICROPY_PY_TREZORPROTO (1) +#define MICROPY_PY_TREZORUI2 (1) #ifdef SYSTEM_VIEW #define MP_PLAT_PRINT_STRN(str, len) segger_print(str, len) diff --git a/core/embed/rust/Cargo.lock b/core/embed/rust/Cargo.lock index c23d2dce1..9085fcb44 100644 --- a/core/embed/rust/Cargo.lock +++ b/core/embed/rust/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "bindgen" -version = "0.58.1" +version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f8523b410d7187a43085e7e064416ea32ded16bd0a4e6fc025e21616d01258f" +checksum = "453c49e5950bb0eb63bb3df640e31618846c89d5b7faa54040d76e98e0134375" dependencies = [ "bitflags", "cexpr", @@ -27,6 +27,24 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitvec" +version = "0.19.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + [[package]] name = "cc" version = "1.0.70" @@ -35,9 +53,9 @@ checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0" [[package]] name = "cexpr" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27" +checksum = "db507a7679252d2276ed0dd8113c6875ec56d3089f9225b2b42c30cc1f8e5c89" dependencies = [ "nom", ] @@ -75,12 +93,38 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7313c0d620d0cb4dbd9d019e461a4beb501071ff46ec0ab933efb4daa76d73e3" +[[package]] +name = "funty" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" + [[package]] name = "glob" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "heapless" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14db22a3fec113074342010bb85a75ba17789244649af8a3178594e0dc97c381" +dependencies = [ + "hash32", + "spin", + "stable_deref_trait", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -109,6 +153,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "lock_api" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" +dependencies = [ + "scopeguard", +] + [[package]] name = "memchr" version = "2.4.1" @@ -117,10 +170,12 @@ checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" [[package]] name = "nom" -version = "5.1.2" +version = "6.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" +checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2" dependencies = [ + "bitvec", + "funty", "memchr", "version_check", ] @@ -149,6 +204,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" + [[package]] name = "regex" version = "1.5.4" @@ -170,12 +231,39 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "shlex" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" +[[package]] +name = "spin" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "511254be0c5bcf062b019a6c89c01a664aa359ded62f78aa72c6fc137c0590e5" +dependencies = [ + "lock_api", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "trezor_lib" version = "0.1.0" @@ -185,6 +273,7 @@ dependencies = [ "cstr_core", "cty", "glob", + "heapless", ] [[package]] @@ -220,3 +309,9 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" diff --git a/core/embed/rust/Cargo.toml b/core/embed/rust/Cargo.toml index 4a6cf7362..07d961128 100644 --- a/core/embed/rust/Cargo.toml +++ b/core/embed/rust/Cargo.toml @@ -8,6 +8,8 @@ build = "build.rs" [features] bitcoin_only = [] +ui = [] +ui_debug = [] test = ["cc", "glob"] [lib] @@ -26,18 +28,22 @@ codegen-units = 1 [dependencies] cty = "0.2.1" +[dependencies.heapless] +version = "0.7.3" +default_features = false + [dependencies.cstr_core] -version = "0.2.2" +version = "0.2.4" default_features = false [build-dependencies.bindgen] -version = "0.58.0" +version = "0.59.1" default_features = false features = ["runtime"] [build-dependencies.cc] optional = true -version = "1.0.68" +version = "1.0.69" [build-dependencies.glob] optional = true diff --git a/core/embed/rust/librust.h b/core/embed/rust/librust.h index 8ece97d85..26bd4d2dd 100644 --- a/core/embed/rust/librust.h +++ b/core/embed/rust/librust.h @@ -11,3 +11,9 @@ mp_obj_t protobuf_encode(mp_obj_t buf, mp_obj_t obj); mp_obj_t protobuf_debug_msg_type(); mp_obj_t protobuf_debug_msg_def_type(); #endif + +mp_obj_t ui_layout_new_example(mp_obj_t); + +#ifdef TREZOR_EMULATOR +mp_obj_t ui_debug_layout_type(); +#endif diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h index 6b0b0cf6b..f187369bc 100644 --- a/core/embed/rust/librust_qstr.h +++ b/core/embed/rust/librust_qstr.h @@ -8,4 +8,14 @@ static void _librust_qstrs(void) { MP_QSTR_is_type_of; MP_QSTR_MESSAGE_WIRE_TYPE; MP_QSTR_MESSAGE_NAME; + + // layout + MP_QSTR_Layout; + MP_QSTR_set_timer_fn; + MP_QSTR_touch_start; + MP_QSTR_touch_move; + MP_QSTR_touch_end; + MP_QSTR_timer; + MP_QSTR_paint; + MP_QSTR_trace; } diff --git a/core/embed/rust/src/error.rs b/core/embed/rust/src/error.rs index 1497ad8e1..c64f4cd3e 100644 --- a/core/embed/rust/src/error.rs +++ b/core/embed/rust/src/error.rs @@ -18,15 +18,13 @@ pub enum Error { } impl Error { - /// Create an exception instance matching the error code. - /// The result of this call should only be used to immediately raise the - /// exception, because the object is not guaranteed to remain intact. - /// Micropython might reuse the same space for creating a different - /// exception. + /// Create an exception instance matching the error code. The result of this + /// call should only be used to immediately raise the exception, because the + /// object is not guaranteed to remain intact. MicroPython might reuse the + /// same space for creating a different exception. pub unsafe fn into_obj(self) -> Obj { unsafe { - // SAFETY: - // - first argument is a reference to a valid exception type + // SAFETY: First argument is a reference to a valid exception type. // EXCEPTION: Sensibly, `new_exception_*` does not raise. match self { Error::TypeError => ffi::mp_obj_new_exception(&ffi::mp_type_TypeError), diff --git a/core/embed/rust/src/lib.rs b/core/embed/rust/src/lib.rs index 19ff60e81..9707427af 100644 --- a/core/embed/rust/src/lib.rs +++ b/core/embed/rust/src/lib.rs @@ -7,7 +7,13 @@ mod error; #[macro_use] mod micropython; mod protobuf; +#[cfg(feature = "ui_debug")] +mod trace; mod trezorhal; + +#[cfg(feature = "ui")] +#[macro_use] +mod ui; mod util; #[cfg(not(test))] @@ -25,8 +31,8 @@ fn panic(_info: &PanicInfo) -> ! { // confusion. // SAFETY: Safe because we are passing in \0-terminated literals. - let empty = unsafe { CStr::from_bytes_with_nul_unchecked("\0".as_bytes()) }; - let msg = unsafe { CStr::from_bytes_with_nul_unchecked("rs\0".as_bytes()) }; + let empty = unsafe { CStr::from_bytes_with_nul_unchecked(b"\0") }; + let msg = unsafe { CStr::from_bytes_with_nul_unchecked(b"rs\0") }; // TODO: Ideally we would take the file and line info out of // `PanicInfo::location()`. diff --git a/core/embed/rust/src/micropython/buffer.rs b/core/embed/rust/src/micropython/buffer.rs index d3da64938..219b56d3d 100644 --- a/core/embed/rust/src/micropython/buffer.rs +++ b/core/embed/rust/src/micropython/buffer.rs @@ -49,6 +49,24 @@ impl AsRef<[u8]> for Buffer { } } +impl From<&'static [u8]> for Buffer { + fn from(val: &'static [u8]) -> Self { + Buffer { + ptr: val.as_ptr(), + len: val.len(), + } + } +} + +impl From<&'static str> for Buffer { + fn from(val: &'static str) -> Self { + Buffer { + ptr: val.as_ptr(), + len: val.len(), + } + } +} + /// Represents a mutable slice of bytes stored on the MicroPython heap and /// owned by values that obey the `MP_BUFFER_WRITE` buffer protocol, such as /// `bytearray` or `memoryview`. diff --git a/core/embed/rust/src/micropython/gc.rs b/core/embed/rust/src/micropython/gc.rs index 865498d64..466edb305 100644 --- a/core/embed/rust/src/micropython/gc.rs +++ b/core/embed/rust/src/micropython/gc.rs @@ -36,20 +36,6 @@ impl Gc { Ok(Self::from_raw(typed)) } } - - /// Return a mutable reference to the value. - /// - /// # Safety - /// - /// `Gc` values can originate in the MicroPython interpreter, and these can - /// be both shared and mutable. Before calling this function, you have to - /// ensure that `this` is unique for the whole lifetime of the - /// returned mutable reference. - pub unsafe fn as_mut(this: &mut Self) -> &mut T { - // SAFETY: The caller must guarantee that `this` meets all the requirements for - // a mutable reference. - unsafe { this.0.as_mut() } - } } impl Gc { @@ -73,6 +59,20 @@ impl Gc { pub fn into_raw(this: Self) -> *mut T { this.0.as_ptr() } + + /// Return a mutable reference to the value. + /// + /// # Safety + /// + /// `Gc` values can originate in the MicroPython interpreter, and these can + /// be both shared and mutable. Before calling this function, you have to + /// ensure that `this` is unique for the whole lifetime of the + /// returned mutable reference. + pub unsafe fn as_mut(this: &mut Self) -> &mut T { + // SAFETY: The caller must guarantee that `this` meets all the requirements for + // a mutable reference. + unsafe { this.0.as_mut() } + } } impl Deref for Gc { diff --git a/core/embed/rust/src/protobuf/defs.rs b/core/embed/rust/src/protobuf/defs.rs index ea98f4006..2179cec3f 100644 --- a/core/embed/rust/src/protobuf/defs.rs +++ b/core/embed/rust/src/protobuf/defs.rs @@ -1,4 +1,3 @@ -use crate::error::Error; use core::{mem, slice}; pub struct MsgDef { @@ -111,10 +110,9 @@ struct NameDef { } static ENUM_DEFS: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/../../../../proto_enums.data")); -static MSG_DEFS: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/../../../..//proto_msgs.data")); -static NAME_DEFS: &[u8] = - include_bytes!(concat!(env!("OUT_DIR"), "/../../../..//proto_names.data")); -static WIRE_DEFS: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/../../../..//proto_wire.data")); +static MSG_DEFS: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/../../../../proto_msgs.data")); +static NAME_DEFS: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/../../../../proto_names.data")); +static WIRE_DEFS: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/../../../../proto_wire.data")); pub fn find_name_by_msg_offset(msg_offset: u16) -> Result { let name_defs: &[NameDef] = unsafe { diff --git a/core/embed/rust/src/protobuf/obj.rs b/core/embed/rust/src/protobuf/obj.rs index 82c5ef019..21483eb85 100644 --- a/core/embed/rust/src/protobuf/obj.rs +++ b/core/embed/rust/src/protobuf/obj.rs @@ -63,13 +63,11 @@ impl MsgObj { return Ok(obj); } - // Built-in attribute + // Built-in attribute. match attr { Qstr::MP_QSTR_MESSAGE_WIRE_TYPE => { // Return the wire ID of this message def, or None if not set. - Ok(self - .msg_wire_id - .map_or(Obj::const_none(), |wire_id| wire_id.into())) + Ok(self.msg_wire_id.map_or_else(Obj::const_none, Into::into)) } Qstr::MP_QSTR_MESSAGE_NAME => { // Return the qstr name of this message def @@ -87,7 +85,7 @@ impl MsgObj { fn setattr(&mut self, attr: Qstr, value: Obj) -> Result<(), Error> { if value.is_null() { - // this would be a delattr + // Null value means a delattr operation, reject. return Err(Error::TypeError); } @@ -207,20 +205,20 @@ unsafe extern "C" fn msg_def_obj_attr(self_in: Obj, attr: ffi::qstr, dest: *mut let arg = unsafe { dest.read() }; if !arg.is_null() { - // this would be a setattr + // Null destination would mean a `setattr`. return Err(Error::TypeError); } match attr { Qstr::MP_QSTR_MESSAGE_NAME => { - // Return the qstr name of this message def + // Return the QSTR name of this message def. let name = Qstr::from_u16(find_name_by_msg_offset(this.def.offset)?); unsafe { dest.write(name.into()); }; } Qstr::MP_QSTR_MESSAGE_WIRE_TYPE => { - // Return the wire type of this message def + // Return the wire type of this message def. let wire_id_obj = this .def .wire_id @@ -230,7 +228,7 @@ unsafe extern "C" fn msg_def_obj_attr(self_in: Obj, attr: ffi::qstr, dest: *mut }; } Qstr::MP_QSTR_is_type_of => { - // Return the is_type_of bound method + // Return the `is_type_of` bound method: // dest[0] = function_obj // dest[1] = self unsafe { diff --git a/core/embed/rust/src/trace.rs b/core/embed/rust/src/trace.rs new file mode 100644 index 000000000..2147f4489 --- /dev/null +++ b/core/embed/rust/src/trace.rs @@ -0,0 +1,38 @@ +/// Visitor passed into `Trace` types. +pub trait Tracer { + fn bytes(&mut self, b: &[u8]); + fn string(&mut self, s: &str); + fn symbol(&mut self, name: &str); + fn open(&mut self, name: &str); + fn field(&mut self, name: &str, value: &dyn Trace); + fn close(&mut self); +} + +/// Value that can describe own structure and data using the `Tracer` interface. +pub trait Trace { + fn trace(&self, d: &mut dyn Tracer); +} + +impl Trace for &[u8] { + fn trace(&self, t: &mut dyn Tracer) { + t.bytes(self); + } +} + +impl Trace for &str { + fn trace(&self, t: &mut dyn Tracer) { + t.string(self); + } +} + +impl Trace for Option +where + T: Trace, +{ + fn trace(&self, d: &mut dyn Tracer) { + match self { + Some(v) => v.trace(d), + None => d.symbol("None"), + } + } +} diff --git a/core/embed/rust/src/trezorhal/display.rs b/core/embed/rust/src/trezorhal/display.rs index 8068d8570..b07044d67 100644 --- a/core/embed/rust/src/trezorhal/display.rs +++ b/core/embed/rust/src/trezorhal/display.rs @@ -25,11 +25,34 @@ extern "C" { b: cty::uint16_t, r: cty::uint8_t, ); + fn display_icon( + x: cty::c_int, + y: cty::c_int, + w: cty::c_int, + h: cty::c_int, + data: *const cty::c_void, + len: cty::uint32_t, + fgcolor: cty::uint16_t, + bgcolor: cty::uint16_t, + ); + fn display_toif_info( + data: *const cty::uint8_t, + len: cty::uint32_t, + out_w: *mut cty::uint16_t, + out_h: *mut cty::uint16_t, + out_grayscale: *mut bool, + ) -> bool; } const WIDTH: i32 = 240; const HEIGHT: i32 = 240; +pub struct ToifInfo { + pub width: u16, + pub height: u16, + pub grayscale: bool, +} + pub fn width() -> i32 { WIDTH } @@ -67,3 +90,41 @@ pub fn bar(x: i32, y: i32, w: i32, h: i32, fgcolor: u16) { pub fn bar_radius(x: i32, y: i32, w: i32, h: i32, fgcolor: u16, bgcolor: u16, radius: u8) { unsafe { display_bar_radius(x, y, w, h, fgcolor, bgcolor, radius) } } + +pub fn icon(x: i32, y: i32, w: i32, h: i32, data: &[u8], fgcolor: u16, bgcolor: u16) { + unsafe { + display_icon( + x, + y, + w, + h, + data.as_ptr() as _, + data.len() as _, + fgcolor, + bgcolor, + ) + } +} + +pub fn toif_info(data: &[u8]) -> Result { + let mut width: cty::uint16_t = 0; + let mut height: cty::uint16_t = 0; + let mut grayscale: bool = false; + if unsafe { + display_toif_info( + data.as_ptr() as _, + data.len() as _, + &mut width, + &mut height, + &mut grayscale, + ) + } { + Ok(ToifInfo { + width, + height, + grayscale, + }) + } else { + Err(()) + } +} diff --git a/core/embed/rust/src/ui/component/base.rs b/core/embed/rust/src/ui/component/base.rs new file mode 100644 index 000000000..3c593bba3 --- /dev/null +++ b/core/embed/rust/src/ui/component/base.rs @@ -0,0 +1,151 @@ +use core::{mem, time::Duration}; + +use heapless::Vec; + +use crate::ui::geometry::Point; + +/// Type used by components that do not return any messages. +/// +/// Alternative to the yet-unstable `!`-type. +pub enum Never {} + +pub trait Component { + type Msg; + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option; + fn paint(&mut self); +} + +pub struct Child { + component: T, + marked_for_paint: bool, +} + +impl Child { + pub fn new(component: T) -> Self { + Self { + component, + marked_for_paint: true, + } + } + + pub fn inner(&self) -> &T { + &self.component + } + + pub fn into_inner(self) -> T { + self.component + } + + pub fn mutate(&mut self, ctx: &mut EventCtx, component_func: F) -> U + where + F: FnOnce(&mut EventCtx, &mut T) -> U, + { + let paint_was_previously_requested = mem::replace(&mut ctx.paint_requested, false); + let component_result = component_func(ctx, &mut self.component); + if ctx.paint_requested { + self.marked_for_paint = true; + } else { + ctx.paint_requested = paint_was_previously_requested; + } + component_result + } +} + +impl Component for Child +where + T: Component, +{ + type Msg = T::Msg; + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + self.mutate(ctx, |ctx, c| c.event(ctx, event)) + } + + fn paint(&mut self) { + if self.marked_for_paint { + self.marked_for_paint = false; + self.component.paint(); + } + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for Child +where + T: crate::trace::Trace, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + self.component.trace(t) + } +} + +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum Event { + TouchStart(Point), + TouchMove(Point), + TouchEnd(Point), + Timer(TimerToken), +} + +#[derive(Copy, Clone, PartialEq, Eq)] +pub struct TimerToken(usize); + +impl TimerToken { + /// Value of an invalid (or missing) token. + pub const INVALID: TimerToken = TimerToken(0); + + pub fn from_raw(raw: usize) -> Self { + Self(raw) + } + + pub fn into_raw(self) -> usize { + self.0 + } +} + +pub struct EventCtx { + timers: Vec<(TimerToken, Duration), { Self::MAX_TIMERS }>, + next_token: usize, + paint_requested: bool, +} + +impl EventCtx { + /// Maximum amount of timers requested in one event tick. + const MAX_TIMERS: usize = 4; + + pub fn new() -> Self { + Self { + timers: Vec::new(), + next_token: 1, + paint_requested: false, + } + } + + pub fn request_paint(&mut self) { + self.paint_requested = true; + } + + pub fn clear_paint_requests(&mut self) { + self.paint_requested = false; + } + + pub fn request_timer(&mut self, deadline: Duration) -> TimerToken { + let token = self.next_timer_token(); + if self.timers.push((token, deadline)).is_err() { + // The timer queue is full. Let's just ignore this request. + #[cfg(feature = "ui_debug")] + panic!("Timer queue is full"); + } + token + } + + pub fn pop_timer(&mut self) -> Option<(TimerToken, Duration)> { + self.timers.pop() + } + + fn next_timer_token(&mut self) -> TimerToken { + let token = TimerToken(self.next_token); + self.next_token += 1; + token + } +} diff --git a/core/embed/rust/src/ui/component/mod.rs b/core/embed/rust/src/ui/component/mod.rs new file mode 100644 index 000000000..2159f31f9 --- /dev/null +++ b/core/embed/rust/src/ui/component/mod.rs @@ -0,0 +1,5 @@ +mod base; +pub mod model_t1; +pub mod model_tt; + +pub use base::{Child, Component, Event, EventCtx, Never, TimerToken}; diff --git a/core/embed/rust/src/ui/component/model_t1/mod.rs b/core/embed/rust/src/ui/component/model_t1/mod.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/core/embed/rust/src/ui/component/model_t1/mod.rs @@ -0,0 +1 @@ + diff --git a/core/embed/rust/src/ui/component/model_tt/button.rs b/core/embed/rust/src/ui/component/model_tt/button.rs new file mode 100644 index 000000000..a58700155 --- /dev/null +++ b/core/embed/rust/src/ui/component/model_tt/button.rs @@ -0,0 +1,224 @@ +use crate::ui::{ + component::{Component, Event, EventCtx}, + display::{self, Color, Font}, + geometry::{Offset, Rect}, +}; + +use super::theme; + +pub enum ButtonMsg { + Clicked, +} + +pub struct Button { + area: Rect, + content: ButtonContent, + styles: ButtonStyleSheet, + state: State, +} + +impl Button { + pub fn new(area: Rect, content: ButtonContent, styles: ButtonStyleSheet) -> Self { + Self { + area, + content, + styles, + state: State::Initial, + } + } + + pub fn with_text(area: Rect, text: &'static [u8], styles: ButtonStyleSheet) -> Self { + Self::new(area, ButtonContent::Text(text), styles) + } + + pub fn with_icon(area: Rect, image: &'static [u8], styles: ButtonStyleSheet) -> Self { + Self::new(area, ButtonContent::Icon(image), styles) + } + + pub fn enable(&mut self, ctx: &mut EventCtx) { + self.set(ctx, State::Initial) + } + + pub fn disable(&mut self, ctx: &mut EventCtx) { + self.set(ctx, State::Disabled) + } + + pub fn is_enabled(&self) -> bool { + matches!( + self.state, + State::Initial | State::Pressed | State::Released + ) + } + + pub fn is_disabled(&self) -> bool { + matches!(self.state, State::Disabled) + } + + pub fn content(&self) -> &ButtonContent { + &self.content + } + + fn style(&self) -> &ButtonStyle { + match self.state { + State::Initial | State::Released => self.styles.normal, + State::Pressed => self.styles.active, + State::Disabled => self.styles.disabled, + } + } + + fn set(&mut self, ctx: &mut EventCtx, state: State) { + if self.state != state { + self.state = state; + ctx.request_paint(); + } + } +} + +impl Component for Button { + type Msg = ButtonMsg; + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + match event { + Event::TouchStart(pos) => { + match self.state { + State::Disabled => { + // Do nothing. + } + _ => { + // Touch started in our area, transform to `Pressed` state. + if self.area.contains(pos) { + self.set(ctx, State::Pressed); + } + } + } + } + Event::TouchMove(pos) => { + match self.state { + State::Released if self.area.contains(pos) => { + // Touch entered our area, transform to `Pressed` state. + self.set(ctx, State::Pressed); + } + State::Pressed if !self.area.contains(pos) => { + // Touch is leaving our area, transform to `Released` state. + self.set(ctx, State::Released); + } + _ => { + // Do nothing. + } + } + } + Event::TouchEnd(pos) => { + match self.state { + State::Initial | State::Disabled => { + // Do nothing. + } + State::Pressed if self.area.contains(pos) => { + // Touch finished in our area, we got clicked. + self.set(ctx, State::Initial); + + return Some(ButtonMsg::Clicked); + } + _ => { + // Touch finished outside our area. + self.set(ctx, State::Initial); + } + } + } + _ => {} + }; + None + } + + fn paint(&mut self) { + let style = self.style(); + + if style.border_width > 0 { + // Paint the border and a smaller background on top of it. + display::rounded_rect( + self.area, + style.border_color, + style.background_color, + style.border_radius, + ); + display::rounded_rect( + self.area.inset(style.border_width), + style.button_color, + style.border_color, + style.border_radius, + ); + } else { + // We do not need to draw an explicit border in this case, just a + // bigger background. + display::rounded_rect( + self.area, + style.button_color, + style.background_color, + style.border_radius, + ); + } + + match &self.content { + ButtonContent::Text(text) => { + let width = display::text_width(text, style.font); + let height = display::text_height(); + let start_of_baseline = self.area.center() + Offset::new(-width / 2, height / 2); + display::text( + start_of_baseline, + text, + style.font, + style.text_color, + style.button_color, + ); + } + ButtonContent::Icon(icon) => { + display::icon( + self.area.center(), + icon, + style.text_color, + style.button_color, + ); + } + } + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for Button { + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.open("Button"); + match self.content { + ButtonContent::Text(text) => t.field("text", &text), + ButtonContent::Icon(_) => t.symbol("icon"), + } + t.close(); + } +} + +#[derive(PartialEq, Eq)] +enum State { + Initial, + Pressed, + Released, + Disabled, +} + +pub enum ButtonContent { + Text(&'static [u8]), + Icon(&'static [u8]), +} + +pub struct ButtonStyleSheet { + pub normal: &'static ButtonStyle, + pub active: &'static ButtonStyle, + pub disabled: &'static ButtonStyle, +} + +pub struct ButtonStyle { + pub font: Font, + pub text_color: Color, + pub button_color: Color, + pub background_color: Color, + pub border_color: Color, + pub border_radius: u8, + pub border_width: i32, +} diff --git a/core/embed/rust/src/ui/component/model_tt/dialog.rs b/core/embed/rust/src/ui/component/model_tt/dialog.rs new file mode 100644 index 000000000..fb8c154d5 --- /dev/null +++ b/core/embed/rust/src/ui/component/model_tt/dialog.rs @@ -0,0 +1,80 @@ +use crate::ui::{ + component::{Child, Component, Event, EventCtx}, + geometry::{Grid, Rect}, +}; + +use super::button::{Button, ButtonMsg::Clicked}; + +pub enum DialogMsg { + Content(T), + LeftClicked, + RightClicked, +} + +pub struct Dialog { + content: Child, + left_btn: Option>, + right_btn: Option>, +} + +impl Dialog { + pub fn new( + area: Rect, + content: impl FnOnce(Rect) -> T, + left: impl FnOnce(Rect) -> Button, + right: impl FnOnce(Rect) -> Button, + ) -> Self { + let grid = Grid::new(area, 5, 2); + let content = Child::new(content(Rect::new( + grid.row_col(0, 0).top_left(), + grid.row_col(4, 1).bottom_right(), + ))); + let left_btn = Child::new(left(grid.row_col(4, 0))); + let right_btn = Child::new(right(grid.row_col(4, 1))); + Self { + content, + left_btn: Some(left_btn), + right_btn: Some(right_btn), + } + } +} + +impl Component for Dialog { + type Msg = DialogMsg; + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + if let Some(msg) = self.content.event(ctx, event) { + Some(DialogMsg::Content(msg)) + } else if let Some(Clicked) = self.left_btn.as_mut().and_then(|b| b.event(ctx, event)) { + Some(DialogMsg::LeftClicked) + } else if let Some(Clicked) = self.right_btn.as_mut().and_then(|b| b.event(ctx, event)) { + Some(DialogMsg::RightClicked) + } else { + None + } + } + + fn paint(&mut self) { + self.content.paint(); + if let Some(b) = self.left_btn.as_mut() { + b.paint(); + } + if let Some(b) = self.right_btn.as_mut() { + b.paint(); + } + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for Dialog +where + T: crate::trace::Trace, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.open("Dialog"); + t.field("content", &self.content); + t.field("left", &self.left_btn); + t.field("right", &self.right_btn); + t.close(); + } +} diff --git a/core/embed/rust/src/ui/component/model_tt/empty.rs b/core/embed/rust/src/ui/component/model_tt/empty.rs new file mode 100644 index 000000000..e87309029 --- /dev/null +++ b/core/embed/rust/src/ui/component/model_tt/empty.rs @@ -0,0 +1,13 @@ +use crate::ui::component::{Component, Event, EventCtx, Never}; + +pub struct Empty; + +impl Component for Empty { + type Msg = Never; + + fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option { + None + } + + fn paint(&mut self) {} +} diff --git a/core/embed/rust/src/ui/component/model_tt/label.rs b/core/embed/rust/src/ui/component/model_tt/label.rs new file mode 100644 index 000000000..d85e00c3a --- /dev/null +++ b/core/embed/rust/src/ui/component/model_tt/label.rs @@ -0,0 +1,90 @@ +use core::ops::Deref; + +use crate::ui::{ + component::{Component, Event, EventCtx, Never}, + display::{self, Color, Font}, + geometry::{Align, Point, Rect}, +}; + +pub struct LabelStyle { + pub font: Font, + pub text_color: Color, + pub background_color: Color, +} + +pub struct Label { + area: Rect, + style: LabelStyle, + text: T, +} + +impl Label +where + T: Deref, +{ + pub fn new(origin: Point, align: Align, text: T, style: LabelStyle) -> Self { + let width = display::text_width(&text, style.font); + let height = display::line_height(); + let area = match align { + // `origin` is the top-left point. + Align::Left => Rect { + x0: origin.x, + y0: origin.y, + x1: origin.x + width, + y1: origin.y + height, + }, + // `origin` is the top-right point. + Align::Right => Rect { + x0: origin.x - width, + y0: origin.y, + x1: origin.x, + y1: origin.y + height, + }, + // `origin` is the top-centered point. + Align::Center => Rect { + x0: origin.x - width / 2, + y0: origin.y, + x1: origin.x + width / 2, + y1: origin.y + height, + }, + }; + Self { area, style, text } + } + + pub fn left_aligned(origin: Point, text: T, style: LabelStyle) -> Self { + Self::new(origin, Align::Left, text, style) + } + + pub fn right_aligned(origin: Point, text: T, style: LabelStyle) -> Self { + Self::new(origin, Align::Right, text, style) + } + + pub fn centered(origin: Point, text: T, style: LabelStyle) -> Self { + Self::new(origin, Align::Center, text, style) + } + + pub fn text(&self) -> &T { + &self.text + } +} + +impl Component for Label +where + T: Deref, +{ + type Msg = Never; + + fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option { + None + } + + fn paint(&mut self) { + display::text( + self.area.bottom_left(), + &self.text, + self.style.font, + self.style.text_color, + self.style.background_color, + ); + } +} diff --git a/core/embed/rust/src/ui/component/model_tt/mod.rs b/core/embed/rust/src/ui/component/model_tt/mod.rs new file mode 100644 index 000000000..46287cde8 --- /dev/null +++ b/core/embed/rust/src/ui/component/model_tt/mod.rs @@ -0,0 +1,17 @@ +mod button; +mod dialog; +mod empty; +mod label; +mod page; +mod passphrase; +mod pin; +mod swipe; +pub mod text; +pub mod theme; + +pub use button::{Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet}; +pub use dialog::{Dialog, DialogMsg}; +pub use empty::Empty; +pub use label::{Label, LabelStyle}; +pub use swipe::{Swipe, SwipeDirection}; +pub use text::{LineBreaking, PageBreaking, Text, TextLayout}; diff --git a/core/embed/rust/src/ui/component/model_tt/page.rs b/core/embed/rust/src/ui/component/model_tt/page.rs new file mode 100644 index 000000000..d62e01e0b --- /dev/null +++ b/core/embed/rust/src/ui/component/model_tt/page.rs @@ -0,0 +1,136 @@ +use crate::ui::{ + component::{Component, Event, EventCtx, Never}, + display, + geometry::{Offset, Point, Rect}, +}; + +use super::{theme, Swipe, SwipeDirection}; + +pub enum PageMsg { + Content(T), + ChangePage(usize), +} + +pub struct Page { + swipe: Swipe, + scrollbar: ScrollBar, + page: T, +} + +impl Page { + pub fn new(area: Rect, page: T, page_count: usize, active_page: usize) -> Self { + let scrollbar = ScrollBar::vertical_right(area, page_count, active_page); + let mut swipe = Swipe::new(area); + swipe.allow_up = scrollbar.has_next_page(); + swipe.allow_down = scrollbar.has_previous_page(); + Self { + swipe, + scrollbar, + page, + } + } +} + +impl Component for Page { + type Msg = PageMsg; + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + if let Some(swipe) = self.swipe.event(ctx, event) { + match swipe { + SwipeDirection::Up => { + // Scroll down, if possible. + return Some(PageMsg::ChangePage(self.scrollbar.next_page())); + } + SwipeDirection::Down => { + // Scroll up, if possible. + return Some(PageMsg::ChangePage(self.scrollbar.previous_page())); + } + _ => { + // Ignore other directions. + } + } + } + if let Some(msg) = self.page.event(ctx, event) { + return Some(PageMsg::Content(msg)); + } + None + } + + fn paint(&mut self) { + self.page.paint(); + self.scrollbar.paint(); + } +} + +pub struct ScrollBar { + area: Rect, + page_count: usize, + active_page: usize, +} + +impl ScrollBar { + pub const DOT_SIZE: Offset = Offset::new(8, 8); + pub const DOT_INTERVAL: i32 = 14; + + pub fn vertical_right(area: Rect, page_count: usize, active_page: usize) -> Self { + Self { + area: area.cut_from_right(Self::DOT_SIZE.x), + page_count, + active_page, + } + } + + pub fn has_next_page(&self) -> bool { + self.active_page < self.page_count - 1 + } + + pub fn has_previous_page(&self) -> bool { + self.active_page > 0 + } + + pub fn next_page(&self) -> usize { + self.active_page.saturating_add(1).min(self.page_count - 1) + } + + pub fn previous_page(&self) -> usize { + self.active_page.saturating_sub(1) + } +} + +impl Component for ScrollBar { + type Msg = Never; + + fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option { + None + } + + fn paint(&mut self) { + let count = self.page_count as i32; + let interval = { + let available_height = self.area.height(); + let naive_height = count * Self::DOT_INTERVAL; + if naive_height > available_height { + available_height / count + } else { + Self::DOT_INTERVAL + } + }; + let mut dot = Point::new( + self.area.center().x, + self.area.center().y - (count / 2) * interval, + ); + for i in 0..self.page_count { + display::rounded_rect( + Rect::from_center_and_size(dot, Self::DOT_SIZE), + if i == self.active_page { + theme::FG + } else { + theme::GREY_LIGHT + }, + theme::BG, + theme::RADIUS, + ); + dot.y += interval; + } + } +} diff --git a/core/embed/rust/src/ui/component/model_tt/passphrase.rs b/core/embed/rust/src/ui/component/model_tt/passphrase.rs new file mode 100644 index 000000000..4b2d4b200 --- /dev/null +++ b/core/embed/rust/src/ui/component/model_tt/passphrase.rs @@ -0,0 +1,319 @@ +use core::time::Duration; + +use heapless::Vec; + +use crate::ui::{ + component::{Child, Component, Event, EventCtx, Never, TimerToken}, + display, + geometry::{Grid, Rect}, +}; + +use super::{ + button::{Button, ButtonContent, ButtonMsg::Clicked}, + swipe::{Swipe, SwipeDirection}, + theme, +}; + +pub enum PassphraseKeyboardMsg { + Confirmed, + Cancelled, +} + +pub struct PassphraseKeyboard { + page_swipe: Swipe, + textbox: Child, + back_btn: Child