mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-02-15 00:52:02 +00:00
feat(core): add Rust UI components, layouts, text rendering
[no changelog]
This commit is contained in:
parent
b905ac04ef
commit
2703d714c2
@ -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)
|
||||
|
@ -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)}"'
|
||||
|
||||
|
@ -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)}"'
|
||||
|
||||
|
@ -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");
|
||||
|
49
core/embed/extmod/rustmods/modtrezorui2.c
Normal file
49
core/embed/extmod/rustmods/modtrezorui2.c
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#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
|
@ -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)
|
||||
|
107
core/embed/rust/Cargo.lock
generated
107
core/embed/rust/Cargo.lock
generated
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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()`.
|
||||
|
@ -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`.
|
||||
|
@ -36,20 +36,6 @@ impl<T> Gc<T> {
|
||||
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<T: ?Sized> Gc<T> {
|
||||
@ -73,6 +59,20 @@ impl<T: ?Sized> Gc<T> {
|
||||
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<T: ?Sized> Deref for Gc<T> {
|
||||
|
@ -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<u16, Error> {
|
||||
let name_defs: &[NameDef] = unsafe {
|
||||
|
@ -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 {
|
||||
|
38
core/embed/rust/src/trace.rs
Normal file
38
core/embed/rust/src/trace.rs
Normal file
@ -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<T> Trace for Option<T>
|
||||
where
|
||||
T: Trace,
|
||||
{
|
||||
fn trace(&self, d: &mut dyn Tracer) {
|
||||
match self {
|
||||
Some(v) => v.trace(d),
|
||||
None => d.symbol("None"),
|
||||
}
|
||||
}
|
||||
}
|
@ -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<ToifInfo, ()> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
151
core/embed/rust/src/ui/component/base.rs
Normal file
151
core/embed/rust/src/ui/component/base.rs
Normal file
@ -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<Self::Msg>;
|
||||
fn paint(&mut self);
|
||||
}
|
||||
|
||||
pub struct Child<T> {
|
||||
component: T,
|
||||
marked_for_paint: bool,
|
||||
}
|
||||
|
||||
impl<T> Child<T> {
|
||||
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<F, U>(&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<T> Component for Child<T>
|
||||
where
|
||||
T: Component,
|
||||
{
|
||||
type Msg = T::Msg;
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
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<T> crate::trace::Trace for Child<T>
|
||||
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
|
||||
}
|
||||
}
|
5
core/embed/rust/src/ui/component/mod.rs
Normal file
5
core/embed/rust/src/ui/component/mod.rs
Normal file
@ -0,0 +1,5 @@
|
||||
mod base;
|
||||
pub mod model_t1;
|
||||
pub mod model_tt;
|
||||
|
||||
pub use base::{Child, Component, Event, EventCtx, Never, TimerToken};
|
1
core/embed/rust/src/ui/component/model_t1/mod.rs
Normal file
1
core/embed/rust/src/ui/component/model_t1/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
|
224
core/embed/rust/src/ui/component/model_tt/button.rs
Normal file
224
core/embed/rust/src/ui/component/model_tt/button.rs
Normal file
@ -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<Self::Msg> {
|
||||
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,
|
||||
}
|
80
core/embed/rust/src/ui/component/model_tt/dialog.rs
Normal file
80
core/embed/rust/src/ui/component/model_tt/dialog.rs
Normal file
@ -0,0 +1,80 @@
|
||||
use crate::ui::{
|
||||
component::{Child, Component, Event, EventCtx},
|
||||
geometry::{Grid, Rect},
|
||||
};
|
||||
|
||||
use super::button::{Button, ButtonMsg::Clicked};
|
||||
|
||||
pub enum DialogMsg<T> {
|
||||
Content(T),
|
||||
LeftClicked,
|
||||
RightClicked,
|
||||
}
|
||||
|
||||
pub struct Dialog<T> {
|
||||
content: Child<T>,
|
||||
left_btn: Option<Child<Button>>,
|
||||
right_btn: Option<Child<Button>>,
|
||||
}
|
||||
|
||||
impl<T: Component> Dialog<T> {
|
||||
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<T: Component> Component for Dialog<T> {
|
||||
type Msg = DialogMsg<T::Msg>;
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
if let Some(msg) = self.content.event(ctx, event) {
|
||||
Some(DialogMsg::Content(msg))
|
||||
} else if let Some(Clicked) = self.left_btn.as_mut().and_then(|b| b.event(ctx, event)) {
|
||||
Some(DialogMsg::LeftClicked)
|
||||
} else if let Some(Clicked) = self.right_btn.as_mut().and_then(|b| b.event(ctx, event)) {
|
||||
Some(DialogMsg::RightClicked)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.content.paint();
|
||||
if let Some(b) = self.left_btn.as_mut() {
|
||||
b.paint();
|
||||
}
|
||||
if let Some(b) = self.right_btn.as_mut() {
|
||||
b.paint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T> crate::trace::Trace for Dialog<T>
|
||||
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();
|
||||
}
|
||||
}
|
13
core/embed/rust/src/ui/component/model_tt/empty.rs
Normal file
13
core/embed/rust/src/ui/component/model_tt/empty.rs
Normal file
@ -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<Self::Msg> {
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {}
|
||||
}
|
90
core/embed/rust/src/ui/component/model_tt/label.rs
Normal file
90
core/embed/rust/src/ui/component/model_tt/label.rs
Normal file
@ -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<T> {
|
||||
area: Rect,
|
||||
style: LabelStyle,
|
||||
text: T,
|
||||
}
|
||||
|
||||
impl<T> Label<T>
|
||||
where
|
||||
T: Deref<Target = [u8]>,
|
||||
{
|
||||
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<T> Component for Label<T>
|
||||
where
|
||||
T: Deref<Target = [u8]>,
|
||||
{
|
||||
type Msg = Never;
|
||||
|
||||
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
display::text(
|
||||
self.area.bottom_left(),
|
||||
&self.text,
|
||||
self.style.font,
|
||||
self.style.text_color,
|
||||
self.style.background_color,
|
||||
);
|
||||
}
|
||||
}
|
17
core/embed/rust/src/ui/component/model_tt/mod.rs
Normal file
17
core/embed/rust/src/ui/component/model_tt/mod.rs
Normal file
@ -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};
|
136
core/embed/rust/src/ui/component/model_tt/page.rs
Normal file
136
core/embed/rust/src/ui/component/model_tt/page.rs
Normal file
@ -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<T> {
|
||||
Content(T),
|
||||
ChangePage(usize),
|
||||
}
|
||||
|
||||
pub struct Page<T> {
|
||||
swipe: Swipe,
|
||||
scrollbar: ScrollBar,
|
||||
page: T,
|
||||
}
|
||||
|
||||
impl<T> Page<T> {
|
||||
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<T: Component> Component for Page<T> {
|
||||
type Msg = PageMsg<T::Msg>;
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
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<Self::Msg> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
319
core/embed/rust/src/ui/component/model_tt/passphrase.rs
Normal file
319
core/embed/rust/src/ui/component/model_tt/passphrase.rs
Normal file
@ -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<TextBox>,
|
||||
back_btn: Child<Button>,
|
||||
confirm_btn: Child<Button>,
|
||||
key_btns: [[Child<Button>; KEYS]; PAGES],
|
||||
key_page: usize,
|
||||
pending: Option<Pending>,
|
||||
}
|
||||
|
||||
struct Pending {
|
||||
key: usize,
|
||||
char: usize,
|
||||
timer: TimerToken,
|
||||
}
|
||||
|
||||
const MAX_LENGTH: usize = 50;
|
||||
const STARTING_PAGE: usize = 1;
|
||||
const PAGES: usize = 4;
|
||||
const KEYS: usize = 10;
|
||||
const PENDING_DEADLINE: Duration = Duration::from_secs(1);
|
||||
|
||||
impl PassphraseKeyboard {
|
||||
pub fn new(area: Rect) -> Self {
|
||||
let textbox_area = Grid::new(area, 5, 1).row_col(0, 0);
|
||||
let confirm_btn_area = Grid::new(area, 5, 3).cell(14);
|
||||
let back_btn_area = Grid::new(area, 5, 3).cell(12);
|
||||
let key_grid = Grid::new(area, 5, 3);
|
||||
|
||||
let text = Vec::new();
|
||||
let page_swipe = Swipe::horizontal(area);
|
||||
let textbox = Child::new(TextBox::new(textbox_area, text));
|
||||
let confirm_btn = Child::new(Button::with_text(
|
||||
confirm_btn_area,
|
||||
b"Confirm",
|
||||
theme::button_confirm(),
|
||||
));
|
||||
let back_btn = Child::new(Button::with_text(
|
||||
back_btn_area,
|
||||
b"Back",
|
||||
theme::button_clear(),
|
||||
));
|
||||
let key_btns = Self::generate_keyboard(&key_grid);
|
||||
|
||||
Self {
|
||||
textbox,
|
||||
page_swipe,
|
||||
confirm_btn,
|
||||
back_btn,
|
||||
key_btns,
|
||||
key_page: STARTING_PAGE,
|
||||
pending: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_keyboard(grid: &Grid) -> [[Child<Button>; KEYS]; PAGES] {
|
||||
[
|
||||
Self::generate_key_page(grid, 0),
|
||||
Self::generate_key_page(grid, 1),
|
||||
Self::generate_key_page(grid, 2),
|
||||
Self::generate_key_page(grid, 3),
|
||||
]
|
||||
}
|
||||
|
||||
fn generate_key_page(grid: &Grid, page: usize) -> [Child<Button>; KEYS] {
|
||||
[
|
||||
Self::generate_key(grid, page, 0),
|
||||
Self::generate_key(grid, page, 1),
|
||||
Self::generate_key(grid, page, 2),
|
||||
Self::generate_key(grid, page, 3),
|
||||
Self::generate_key(grid, page, 4),
|
||||
Self::generate_key(grid, page, 5),
|
||||
Self::generate_key(grid, page, 6),
|
||||
Self::generate_key(grid, page, 7),
|
||||
Self::generate_key(grid, page, 8),
|
||||
Self::generate_key(grid, page, 9),
|
||||
]
|
||||
}
|
||||
|
||||
fn generate_key(grid: &Grid, page: usize, key: usize) -> Child<Button> {
|
||||
#[rustfmt::skip]
|
||||
const KEYBOARD: [[&str; KEYS]; PAGES] = [
|
||||
["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
|
||||
[" ", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz", "*#"],
|
||||
[" ", "ABC", "DEF", "GHI", "JKL", "MNO", "PQRS", "TUV", "WXYZ", "*#"],
|
||||
["_<>", ".:@", "/|\\", "!()", "+%&", "-[]", "?{}", ",'`", ";\"~", "$^="],
|
||||
];
|
||||
|
||||
// Assign the keys in each page to buttons on a 5x3 grid, starting from the
|
||||
// second row.
|
||||
let area = grid.cell(if key < 9 {
|
||||
// The grid has 3 columns, and we skip the first row.
|
||||
key + 3
|
||||
} else {
|
||||
// For the last key (the "0" position) we skip one cell.
|
||||
key + 1 + 3
|
||||
});
|
||||
let text = KEYBOARD[page][key].as_bytes();
|
||||
if text == b" " {
|
||||
let icon = theme::ICON_SPACE;
|
||||
Child::new(Button::with_icon(area, icon, theme::button_default()))
|
||||
} else {
|
||||
Child::new(Button::with_text(area, text, theme::button_default()))
|
||||
}
|
||||
}
|
||||
|
||||
fn on_page_swipe(&mut self, swipe: SwipeDirection) {
|
||||
self.key_page = match swipe {
|
||||
SwipeDirection::Left => (self.key_page as isize + 1) as usize % PAGES,
|
||||
SwipeDirection::Right => (self.key_page as isize - 1) as usize % PAGES,
|
||||
_ => self.key_page,
|
||||
};
|
||||
self.pending.take();
|
||||
}
|
||||
|
||||
fn on_backspace_click(&mut self, ctx: &mut EventCtx) {
|
||||
self.pending.take();
|
||||
self.textbox.mutate(ctx, |ctx, t| t.delete_last(ctx));
|
||||
self.after_edit(ctx);
|
||||
}
|
||||
|
||||
fn on_key_click(&mut self, ctx: &mut EventCtx, key: usize) {
|
||||
let content = self.key_content(self.key_page, key);
|
||||
|
||||
let char = match &self.pending {
|
||||
Some(pending) if pending.key == key => {
|
||||
// This key is pending. Cycle the last inserted character through the
|
||||
// key content.
|
||||
let char = (pending.char + 1) % content.len();
|
||||
self.textbox
|
||||
.mutate(ctx, |ctx, t| t.replace_last(ctx, content[char]));
|
||||
char
|
||||
}
|
||||
_ => {
|
||||
// This key is not pending. Append the first character in the key.
|
||||
let char = 0;
|
||||
self.textbox
|
||||
.mutate(ctx, |ctx, t| t.append(ctx, content[char]));
|
||||
char
|
||||
}
|
||||
};
|
||||
|
||||
// If the key has more then one character, we need to set it as pending, so we
|
||||
// can cycle through on the repeated clicks. We also request a timer so we can
|
||||
// reset the pending state after a deadline.
|
||||
self.pending = if content.len() > 1 {
|
||||
Some(Pending {
|
||||
key,
|
||||
char,
|
||||
timer: ctx.request_timer(PENDING_DEADLINE),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let is_pending = self.pending.is_some();
|
||||
self.textbox
|
||||
.mutate(ctx, |ctx, t| t.toggle_pending_marker(ctx, is_pending));
|
||||
|
||||
self.after_edit(ctx);
|
||||
}
|
||||
|
||||
fn on_timeout(&mut self) {
|
||||
self.pending.take();
|
||||
}
|
||||
|
||||
fn key_content(&self, page: usize, key: usize) -> &'static [u8] {
|
||||
match self.key_btns[page][key].inner().content() {
|
||||
ButtonContent::Text(text) => text,
|
||||
ButtonContent::Icon(_) => b" ",
|
||||
}
|
||||
}
|
||||
|
||||
fn after_edit(&mut self, ctx: &mut EventCtx) {
|
||||
if self.textbox.inner().is_empty() {
|
||||
self.back_btn.mutate(ctx, |ctx, b| b.disable(ctx));
|
||||
} else {
|
||||
self.back_btn.mutate(ctx, |ctx, b| b.enable(ctx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for PassphraseKeyboard {
|
||||
type Msg = PassphraseKeyboardMsg;
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
if matches!((event, &self.pending), (Event::Timer(t), Some(p)) if p.timer == t) {
|
||||
// Our pending timer triggered, reset the pending state.
|
||||
self.on_timeout();
|
||||
return None;
|
||||
}
|
||||
if let Some(swipe) = self.page_swipe.event(ctx, event) {
|
||||
// We have detected a horizontal swipe. Change the keyboard page.
|
||||
self.on_page_swipe(swipe);
|
||||
return None;
|
||||
}
|
||||
if let Some(Clicked) = self.confirm_btn.event(ctx, event) {
|
||||
// Confirm button was clicked, we're done.
|
||||
return Some(PassphraseKeyboardMsg::Confirmed);
|
||||
}
|
||||
if let Some(Clicked) = self.back_btn.event(ctx, event) {
|
||||
// Backspace button was clicked. If we have any content in the textbox, let's
|
||||
// delete the last character. Otherwise cancel.
|
||||
if self.textbox.inner().is_empty() {
|
||||
return Some(PassphraseKeyboardMsg::Cancelled);
|
||||
} else {
|
||||
self.on_backspace_click(ctx);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
for (key, btn) in self.key_btns[self.key_page].iter_mut().enumerate() {
|
||||
if let Some(Clicked) = btn.event(ctx, event) {
|
||||
// Key button was clicked. If this button is pending, let's cycle the pending
|
||||
// character in textbox. If not, let's just append the first character.
|
||||
self.on_key_click(ctx, key);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.textbox.paint();
|
||||
self.confirm_btn.paint();
|
||||
self.back_btn.paint();
|
||||
for btn in &mut self.key_btns[self.key_page] {
|
||||
btn.paint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TextBox {
|
||||
area: Rect,
|
||||
text: Vec<u8, MAX_LENGTH>,
|
||||
pending: bool,
|
||||
}
|
||||
|
||||
impl TextBox {
|
||||
fn new(area: Rect, text: Vec<u8, MAX_LENGTH>) -> Self {
|
||||
Self {
|
||||
area,
|
||||
text,
|
||||
pending: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.text.is_empty()
|
||||
}
|
||||
|
||||
fn toggle_pending_marker(&mut self, ctx: &mut EventCtx, pending: bool) {
|
||||
self.pending = pending;
|
||||
ctx.request_paint();
|
||||
}
|
||||
|
||||
fn delete_last(&mut self, ctx: &mut EventCtx) {
|
||||
self.text.pop();
|
||||
ctx.request_paint();
|
||||
}
|
||||
|
||||
fn replace_last(&mut self, ctx: &mut EventCtx, char: u8) {
|
||||
self.text.pop();
|
||||
if self.text.push(char).is_err() {
|
||||
// Should not happen unless `self.text` has zero capacity.
|
||||
#[cfg(feature = "ui_debug")]
|
||||
panic!("Textbox has zero capacity");
|
||||
}
|
||||
ctx.request_paint();
|
||||
}
|
||||
|
||||
fn append(&mut self, ctx: &mut EventCtx, char: u8) {
|
||||
if self.text.push(char).is_err() {
|
||||
// `self.text` is full, ignore this change.
|
||||
#[cfg(feature = "ui_debug")]
|
||||
panic!("Textbox is full");
|
||||
}
|
||||
ctx.request_paint();
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for TextBox {
|
||||
type Msg = Never;
|
||||
|
||||
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
let style = theme::label_default();
|
||||
|
||||
display::text(
|
||||
self.area.bottom_left(),
|
||||
&self.text,
|
||||
style.font,
|
||||
style.text_color,
|
||||
style.background_color,
|
||||
);
|
||||
}
|
||||
}
|
256
core/embed/rust/src/ui/component/model_tt/pin.rs
Normal file
256
core/embed/rust/src/ui/component/model_tt/pin.rs
Normal file
@ -0,0 +1,256 @@
|
||||
use heapless::Vec;
|
||||
|
||||
use crate::{
|
||||
trezorhal::random,
|
||||
ui::{
|
||||
component::{Child, Component, Event, EventCtx, Never},
|
||||
display,
|
||||
geometry::{Grid, Offset, Point, Rect},
|
||||
},
|
||||
};
|
||||
|
||||
use super::{
|
||||
button::{Button, ButtonContent, ButtonMsg::Clicked},
|
||||
label::{Label, LabelStyle},
|
||||
theme,
|
||||
};
|
||||
|
||||
pub enum PinDialogMsg {
|
||||
Confirmed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
const MAX_LENGTH: usize = 9;
|
||||
const DIGIT_COUNT: usize = 10; // 0..10
|
||||
|
||||
pub struct PinDialog {
|
||||
digits: Vec<u8, MAX_LENGTH>,
|
||||
major_prompt: Label<&'static [u8]>,
|
||||
minor_prompt: Label<&'static [u8]>,
|
||||
dots: Child<PinDots>,
|
||||
reset_btn: Child<Button>,
|
||||
cancel_btn: Child<Button>,
|
||||
confirm_btn: Child<Button>,
|
||||
digit_btns: [Child<Button>; DIGIT_COUNT],
|
||||
}
|
||||
|
||||
impl PinDialog {
|
||||
pub fn new(area: Rect, major_prompt: &'static [u8], minor_prompt: &'static [u8]) -> Self {
|
||||
let digits = Vec::new();
|
||||
|
||||
// Prompts and PIN dots display.
|
||||
let grid = if minor_prompt.is_empty() {
|
||||
// Make the major prompt bigger if the minor one is empty.
|
||||
Grid::new(area, 5, 1)
|
||||
} else {
|
||||
Grid::new(area, 6, 1)
|
||||
};
|
||||
let major_prompt = Label::centered(
|
||||
grid.row_col(0, 0).center(),
|
||||
major_prompt,
|
||||
theme::label_default(),
|
||||
);
|
||||
let minor_prompt = Label::centered(
|
||||
grid.row_col(0, 1).center(),
|
||||
minor_prompt,
|
||||
theme::label_default(),
|
||||
);
|
||||
let dots = Child::new(PinDots::new(
|
||||
grid.row_col(0, 0),
|
||||
digits.len(),
|
||||
theme::label_default(),
|
||||
));
|
||||
|
||||
// Control buttons.
|
||||
let grid = Grid::new(area, 5, 3);
|
||||
let reset_btn = Child::new(Button::with_text(
|
||||
grid.row_col(4, 0),
|
||||
b"Reset",
|
||||
theme::button_clear(),
|
||||
));
|
||||
let cancel_btn = Child::new(Button::with_icon(
|
||||
grid.row_col(4, 0),
|
||||
theme::ICON_CANCEL,
|
||||
theme::button_cancel(),
|
||||
));
|
||||
let confirm_btn = Child::new(Button::with_icon(
|
||||
grid.row_col(4, 2),
|
||||
theme::ICON_CONFIRM,
|
||||
theme::button_clear(),
|
||||
));
|
||||
|
||||
// PIN digit buttons.
|
||||
let digit_btns = Self::generate_digit_buttons(&grid);
|
||||
|
||||
Self {
|
||||
digits,
|
||||
major_prompt,
|
||||
minor_prompt,
|
||||
dots,
|
||||
reset_btn,
|
||||
cancel_btn,
|
||||
confirm_btn,
|
||||
digit_btns,
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_digit_buttons(grid: &Grid) -> [Child<Button>; DIGIT_COUNT] {
|
||||
// Generate a random sequence of digits from 0 to 9.
|
||||
let mut digits = [b"0", b"1", b"2", b"3", b"4", b"5", b"6", b"7", b"8", b"9"];
|
||||
random::shuffle(&mut digits);
|
||||
|
||||
// Assign the digits to buttons on a 5x3 grid, starting from the second row.
|
||||
let btn = |i| {
|
||||
let area = grid.cell(if i < 9 {
|
||||
// The grid has 3 columns, and we skip the first row.
|
||||
i + 3
|
||||
} else {
|
||||
// For the last key (the "0" position) we skip one cell.
|
||||
i + 1 + 3
|
||||
});
|
||||
let text: &[u8; 1] = digits[i];
|
||||
Child::new(Button::with_text(area, text, theme::button_default()))
|
||||
};
|
||||
[
|
||||
btn(0),
|
||||
btn(1),
|
||||
btn(2),
|
||||
btn(3),
|
||||
btn(4),
|
||||
btn(5),
|
||||
btn(6),
|
||||
btn(7),
|
||||
btn(8),
|
||||
btn(9),
|
||||
]
|
||||
}
|
||||
|
||||
fn pin_modified(&mut self, ctx: &mut EventCtx) {
|
||||
for btn in &mut self.digit_btns {
|
||||
let is_full = self.digits.is_full();
|
||||
btn.mutate(ctx, |ctx, btn| {
|
||||
if is_full {
|
||||
btn.disable(ctx);
|
||||
} else {
|
||||
btn.enable(ctx);
|
||||
}
|
||||
});
|
||||
}
|
||||
if self.digits.is_empty() {
|
||||
self.reset_btn.mutate(ctx, |ctx, btn| btn.disable(ctx));
|
||||
self.cancel_btn.mutate(ctx, |ctx, btn| btn.enable(ctx));
|
||||
self.confirm_btn.mutate(ctx, |ctx, btn| btn.disable(ctx));
|
||||
} else {
|
||||
self.reset_btn.mutate(ctx, |ctx, btn| btn.enable(ctx));
|
||||
self.cancel_btn.mutate(ctx, |ctx, btn| btn.disable(ctx));
|
||||
self.confirm_btn.mutate(ctx, |ctx, btn| btn.enable(ctx));
|
||||
}
|
||||
let digit_count = self.digits.len();
|
||||
self.dots
|
||||
.mutate(ctx, |ctx, dots| dots.update(ctx, digit_count));
|
||||
}
|
||||
|
||||
pub fn pin(&self) -> &[u8] {
|
||||
&self.digits
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for PinDialog {
|
||||
type Msg = PinDialogMsg;
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
if let Some(Clicked) = self.confirm_btn.event(ctx, event) {
|
||||
return Some(PinDialogMsg::Confirmed);
|
||||
}
|
||||
if let Some(Clicked) = self.cancel_btn.event(ctx, event) {
|
||||
return Some(PinDialogMsg::Cancelled);
|
||||
}
|
||||
if let Some(Clicked) = self.reset_btn.event(ctx, event) {
|
||||
self.digits.clear();
|
||||
self.pin_modified(ctx);
|
||||
return None;
|
||||
}
|
||||
for btn in &mut self.digit_btns {
|
||||
if let Some(Clicked) = btn.event(ctx, event) {
|
||||
if let ButtonContent::Text(text) = btn.inner().content() {
|
||||
if self.digits.extend_from_slice(text).is_err() {
|
||||
// `self.pin` is full and wasn't able to accept all of
|
||||
// `text`. Should not happen.
|
||||
}
|
||||
self.pin_modified(ctx);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
if self.digits.is_empty() {
|
||||
self.cancel_btn.paint();
|
||||
self.major_prompt.paint();
|
||||
self.minor_prompt.paint();
|
||||
} else {
|
||||
self.reset_btn.paint();
|
||||
self.dots.paint();
|
||||
}
|
||||
self.confirm_btn.paint();
|
||||
for btn in &mut self.digit_btns {
|
||||
btn.paint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PinDots {
|
||||
area: Rect,
|
||||
style: LabelStyle,
|
||||
digit_count: usize,
|
||||
}
|
||||
|
||||
impl PinDots {
|
||||
const DOT: i32 = 10;
|
||||
const PADDING: i32 = 4;
|
||||
|
||||
fn new(area: Rect, digit_count: usize, style: LabelStyle) -> Self {
|
||||
Self {
|
||||
area,
|
||||
style,
|
||||
digit_count,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &mut EventCtx, digit_count: usize) {
|
||||
if digit_count != self.digit_count {
|
||||
self.digit_count = digit_count;
|
||||
ctx.request_paint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for PinDots {
|
||||
type Msg = Never;
|
||||
|
||||
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
// Clear the area with the background color.
|
||||
display::rect(self.area, self.style.background_color);
|
||||
|
||||
// Draw a dot for each PIN digit.
|
||||
for i in 0..self.digit_count {
|
||||
let pos = Point {
|
||||
x: self.area.x0 + i as i32 * (Self::DOT + Self::PADDING),
|
||||
y: self.area.center().y,
|
||||
};
|
||||
let size = Offset::new(Self::DOT, Self::DOT);
|
||||
display::rounded_rect(
|
||||
Rect::from_top_left_and_size(pos, size),
|
||||
self.style.text_color,
|
||||
self.style.background_color,
|
||||
4,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
149
core/embed/rust/src/ui/component/model_tt/swipe.rs
Normal file
149
core/embed/rust/src/ui/component/model_tt/swipe.rs
Normal file
@ -0,0 +1,149 @@
|
||||
use crate::ui::{
|
||||
component::{Component, Event, EventCtx},
|
||||
display,
|
||||
geometry::{Point, Rect},
|
||||
};
|
||||
|
||||
use super::theme;
|
||||
|
||||
pub enum SwipeDirection {
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
pub struct Swipe {
|
||||
area: Rect,
|
||||
pub allow_up: bool,
|
||||
pub allow_down: bool,
|
||||
pub allow_left: bool,
|
||||
pub allow_right: bool,
|
||||
backlight_start: i32,
|
||||
backlight_end: i32,
|
||||
origin: Option<Point>,
|
||||
}
|
||||
|
||||
impl Swipe {
|
||||
const DISTANCE: i32 = 120;
|
||||
const THRESHOLD: f32 = 0.3;
|
||||
|
||||
pub fn new(area: Rect) -> Self {
|
||||
Self {
|
||||
area,
|
||||
allow_up: false,
|
||||
allow_down: false,
|
||||
allow_left: false,
|
||||
allow_right: false,
|
||||
backlight_start: theme::BACKLIGHT_NORMAL,
|
||||
backlight_end: theme::BACKLIGHT_NONE,
|
||||
origin: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn vertical(area: Rect) -> Self {
|
||||
Self::new(area).up().down()
|
||||
}
|
||||
|
||||
pub fn horizontal(area: Rect) -> Self {
|
||||
Self::new(area).left().right()
|
||||
}
|
||||
|
||||
pub fn up(mut self) -> Self {
|
||||
self.allow_up = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn down(mut self) -> Self {
|
||||
self.allow_down = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn left(mut self) -> Self {
|
||||
self.allow_left = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn right(mut self) -> Self {
|
||||
self.allow_right = true;
|
||||
self
|
||||
}
|
||||
|
||||
fn ratio(&self, dist: i32) -> f32 {
|
||||
(dist as f32 / Self::DISTANCE as f32).min(1.0)
|
||||
}
|
||||
|
||||
fn backlight(&self, ratio: f32) {
|
||||
let start = self.backlight_start as f32;
|
||||
let end = self.backlight_end as f32;
|
||||
let value = start + ratio * (end - start);
|
||||
display::backlight(value as i32);
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Swipe {
|
||||
type Msg = SwipeDirection;
|
||||
|
||||
fn event(&mut self, _ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
match (event, self.origin) {
|
||||
(Event::TouchStart(pos), _) if self.area.contains(pos) => {
|
||||
// Mark the starting position of this touch.
|
||||
self.origin.replace(pos);
|
||||
}
|
||||
(Event::TouchMove(pos), Some(origin)) => {
|
||||
// Consider our allowed directions and the touch distance and modify the display
|
||||
// backlight accordingly.
|
||||
let ofs = pos - origin;
|
||||
let abs = ofs.abs();
|
||||
if abs.x > abs.y && (self.allow_left || self.allow_right) {
|
||||
// Horizontal direction.
|
||||
if (ofs.x < 0 && self.allow_left) || (ofs.x > 0 && self.allow_right) {
|
||||
self.backlight(self.ratio(abs.x));
|
||||
}
|
||||
} else if abs.x < abs.y && (self.allow_up || self.allow_down) {
|
||||
// Vertical direction.
|
||||
if (ofs.y < 0 && self.allow_up) || (ofs.y > 0 && self.allow_down) {
|
||||
self.backlight(self.ratio(abs.y));
|
||||
}
|
||||
};
|
||||
}
|
||||
(Event::TouchEnd(pos), Some(origin)) => {
|
||||
// Touch interaction is over, reset the position.
|
||||
self.origin.take();
|
||||
|
||||
// Compare the touch distance with our allowed directions and determine if it
|
||||
// constitutes a valid swipe.
|
||||
let ofs = pos - origin;
|
||||
let abs = ofs.abs();
|
||||
if abs.x > abs.y && (self.allow_left || self.allow_right) {
|
||||
// Horizontal direction.
|
||||
if self.ratio(abs.x) >= Self::THRESHOLD {
|
||||
if ofs.x < 0 && self.allow_left {
|
||||
return Some(SwipeDirection::Left);
|
||||
} else if ofs.x > 0 && self.allow_right {
|
||||
return Some(SwipeDirection::Right);
|
||||
}
|
||||
}
|
||||
} else if abs.x < abs.y && (self.allow_up || self.allow_down) {
|
||||
// Vertical direction.
|
||||
if self.ratio(abs.y) >= Self::THRESHOLD {
|
||||
if ofs.y < 0 && self.allow_up {
|
||||
return Some(SwipeDirection::Up);
|
||||
} else if ofs.y > 0 && self.allow_down {
|
||||
return Some(SwipeDirection::Down);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Swipe did not happen, reset the backlight.
|
||||
self.backlight(0.0);
|
||||
}
|
||||
_ => {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {}
|
||||
}
|
647
core/embed/rust/src/ui/component/model_tt/text.rs
Normal file
647
core/embed/rust/src/ui/component/model_tt/text.rs
Normal file
@ -0,0 +1,647 @@
|
||||
use core::{
|
||||
iter::{Enumerate, Peekable},
|
||||
slice,
|
||||
};
|
||||
|
||||
use heapless::LinearMap;
|
||||
|
||||
use crate::ui::{
|
||||
component::{Component, Event, EventCtx, Never},
|
||||
display,
|
||||
display::{Color, Font},
|
||||
geometry::{Offset, Point, Rect},
|
||||
};
|
||||
|
||||
use super::theme;
|
||||
|
||||
pub const MAX_ARGUMENTS: usize = 6;
|
||||
|
||||
pub struct Text<F, T> {
|
||||
layout: TextLayout,
|
||||
format: F,
|
||||
args: LinearMap<&'static [u8], T, MAX_ARGUMENTS>,
|
||||
}
|
||||
|
||||
impl<F, T> Text<F, T> {
|
||||
pub fn new(area: Rect, format: F) -> Self {
|
||||
Self {
|
||||
layout: TextLayout::new(area),
|
||||
format,
|
||||
args: LinearMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format(mut self, format: F) -> Self {
|
||||
self.format = format;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with(mut self, key: &'static [u8], value: T) -> Self {
|
||||
if self.args.insert(key, value).is_err() {
|
||||
// Map is full, ignore.
|
||||
#[cfg(feature = "ui_debug")]
|
||||
panic!("Text args map is full");
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_text_font(mut self, text_font: Font) -> Self {
|
||||
self.layout.text_font = text_font;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_text_color(mut self, text_color: Color) -> Self {
|
||||
self.layout.text_color = text_color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_line_breaking(mut self, line_breaking: LineBreaking) -> Self {
|
||||
self.layout.line_breaking = line_breaking;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_page_breaking(mut self, page_breaking: PageBreaking) -> Self {
|
||||
self.layout.page_breaking = page_breaking;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn layout_mut(&mut self) -> &mut TextLayout {
|
||||
&mut self.layout
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, T> Text<F, T>
|
||||
where
|
||||
F: AsRef<[u8]>,
|
||||
T: AsRef<[u8]>,
|
||||
{
|
||||
fn layout_content(&self, sink: &mut dyn LayoutSink) {
|
||||
self.layout.layout_formatted(
|
||||
self.format.as_ref(),
|
||||
|arg| match arg {
|
||||
Token::Literal(literal) => Some(Op::Text(literal)),
|
||||
Token::Argument(b"mono") => Some(Op::Font(theme::FONT_MONO)),
|
||||
Token::Argument(b"bold") => Some(Op::Font(theme::FONT_BOLD)),
|
||||
Token::Argument(b"normal") => Some(Op::Font(theme::FONT_NORMAL)),
|
||||
Token::Argument(argument) => self
|
||||
.args
|
||||
.get(argument)
|
||||
.map(|value| Op::Text(value.as_ref())),
|
||||
},
|
||||
sink,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, T> Component for Text<F, T>
|
||||
where
|
||||
F: AsRef<[u8]>,
|
||||
T: AsRef<[u8]>,
|
||||
{
|
||||
type Msg = Never;
|
||||
|
||||
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.layout_content(&mut TextRenderer);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
mod trace {
|
||||
use super::*;
|
||||
|
||||
pub struct TraceSink<'a>(pub &'a mut dyn crate::trace::Tracer);
|
||||
|
||||
impl<'a> LayoutSink for TraceSink<'a> {
|
||||
fn text(&mut self, _cursor: Point, _layout: &TextLayout, text: &[u8]) {
|
||||
self.0.bytes(text);
|
||||
}
|
||||
|
||||
fn hyphen(&mut self, _cursor: Point, _layout: &TextLayout) {
|
||||
self.0.string("-");
|
||||
}
|
||||
|
||||
fn ellipsis(&mut self, _cursor: Point, _layout: &TextLayout) {
|
||||
self.0.string("...");
|
||||
}
|
||||
|
||||
fn line_break(&mut self, _cursor: Point) {
|
||||
self.0.string("\n");
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TraceText<'a, F, T>(pub &'a Text<F, T>);
|
||||
|
||||
impl<'a, F, T> crate::trace::Trace for TraceText<'a, F, T>
|
||||
where
|
||||
F: AsRef<[u8]>,
|
||||
T: AsRef<[u8]>,
|
||||
{
|
||||
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
|
||||
self.0.layout_content(&mut TraceSink(d));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<F, T> crate::trace::Trace for Text<F, T>
|
||||
where
|
||||
F: AsRef<[u8]>,
|
||||
T: AsRef<[u8]>,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("Text");
|
||||
t.field("content", &trace::TraceText(self));
|
||||
t.close();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum LineBreaking {
|
||||
/// Break line only at whitespace, if possible. If we don't find any
|
||||
/// whitespace, break words.
|
||||
BreakAtWhitespace,
|
||||
/// Break words, adding a hyphen before the line-break. Does not use any
|
||||
/// smart algorithm, just char-by-char.
|
||||
BreakWordsAndInsertHyphen,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum PageBreaking {
|
||||
/// Stop after hitting the bottom-right edge of the bounds.
|
||||
Cut,
|
||||
/// Before stopping at the bottom-right edge, insert ellipsis to signify
|
||||
/// more content is available, but only if no hyphen has been inserted yet.
|
||||
CutAndInsertEllipsis,
|
||||
}
|
||||
|
||||
/// Visual instructions for laying out a formatted block of text.
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct TextLayout {
|
||||
/// Bounding box restricting the layout dimensions.
|
||||
pub bounds: Rect,
|
||||
|
||||
/// Background color.
|
||||
pub background_color: Color,
|
||||
/// Text color. Can be overridden by `Op::Color`.
|
||||
pub text_color: Color,
|
||||
/// Text font ID. Can be overridden by `Op::Font`.
|
||||
pub text_font: Font,
|
||||
|
||||
/// Specifies which line-breaking strategy to use.
|
||||
pub line_breaking: LineBreaking,
|
||||
/// Font used for drawing the word-breaking hyphen.
|
||||
pub hyphen_font: Font,
|
||||
/// Foreground color used for drawing the hyphen.
|
||||
pub hyphen_color: Color,
|
||||
|
||||
/// Specifies what to do at the end of the page.
|
||||
pub page_breaking: PageBreaking,
|
||||
/// Font used for drawing the ellipsis.
|
||||
pub ellipsis_font: Font,
|
||||
/// Foreground color used for drawing the ellipsis.
|
||||
pub ellipsis_color: Color,
|
||||
}
|
||||
|
||||
impl TextLayout {
|
||||
pub fn new(bounds: Rect) -> Self {
|
||||
Self {
|
||||
bounds,
|
||||
background_color: theme::BG,
|
||||
text_color: theme::FG,
|
||||
text_font: theme::FONT_NORMAL,
|
||||
line_breaking: LineBreaking::BreakAtWhitespace,
|
||||
hyphen_font: theme::FONT_BOLD,
|
||||
hyphen_color: theme::GREY_LIGHT,
|
||||
page_breaking: PageBreaking::CutAndInsertEllipsis,
|
||||
ellipsis_font: theme::FONT_BOLD,
|
||||
ellipsis_color: theme::GREY_LIGHT,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initial_cursor(&self) -> Point {
|
||||
Point::new(
|
||||
self.bounds.top_left().x,
|
||||
self.bounds.top_left().y + self.text_font.line_height(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn layout_formatted<'f, 'o, F, I>(
|
||||
self,
|
||||
format: &'f [u8],
|
||||
resolve: F,
|
||||
sink: &mut dyn LayoutSink,
|
||||
) -> LayoutFit
|
||||
where
|
||||
F: Fn(Token<'f>) -> I,
|
||||
I: IntoIterator<Item = Op<'o>>,
|
||||
{
|
||||
let mut cursor = self.initial_cursor();
|
||||
|
||||
self.layout_op_stream(
|
||||
&mut Tokenizer::new(format).flat_map(resolve),
|
||||
&mut cursor,
|
||||
sink,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn layout_op_stream<'o>(
|
||||
mut self,
|
||||
ops: &mut dyn Iterator<Item = Op<'o>>,
|
||||
cursor: &mut Point,
|
||||
sink: &mut dyn LayoutSink,
|
||||
) -> LayoutFit {
|
||||
let mut total_processed_chars = 0;
|
||||
|
||||
for op in ops {
|
||||
match op {
|
||||
Op::Color(color) => {
|
||||
self.text_color = color;
|
||||
}
|
||||
Op::Font(font) => {
|
||||
self.text_font = font;
|
||||
}
|
||||
Op::Text(text) => match self.layout_text(text, cursor, sink) {
|
||||
LayoutFit::Fitting { processed_chars } => {
|
||||
total_processed_chars += processed_chars;
|
||||
}
|
||||
LayoutFit::OutOfBounds { processed_chars } => {
|
||||
total_processed_chars += processed_chars;
|
||||
|
||||
return LayoutFit::OutOfBounds {
|
||||
processed_chars: total_processed_chars,
|
||||
};
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
LayoutFit::Fitting {
|
||||
processed_chars: total_processed_chars,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn layout_text(
|
||||
&self,
|
||||
text: &[u8],
|
||||
cursor: &mut Point,
|
||||
sink: &mut dyn LayoutSink,
|
||||
) -> LayoutFit {
|
||||
let mut remaining_text = text;
|
||||
|
||||
while !remaining_text.is_empty() {
|
||||
let span = Span::fit_horizontally(
|
||||
remaining_text,
|
||||
self.bounds.x1 - cursor.x,
|
||||
self.text_font,
|
||||
self.hyphen_font,
|
||||
self.line_breaking,
|
||||
);
|
||||
|
||||
// Report the span at the cursor position.
|
||||
sink.text(*cursor, self, &remaining_text[..span.length]);
|
||||
|
||||
// Continue with the rest of the remaining_text.
|
||||
remaining_text = &remaining_text[span.length + span.skip_next_chars..];
|
||||
|
||||
// Advance the cursor horizontally.
|
||||
cursor.x += span.advance.x;
|
||||
|
||||
if span.advance.y > 0 {
|
||||
// We're advancing to the next line.
|
||||
|
||||
// Check if we should be appending a hyphen at this point.
|
||||
if span.insert_hyphen_before_line_break {
|
||||
sink.hyphen(*cursor, self);
|
||||
}
|
||||
// Check the amount of vertical space we have left.
|
||||
if cursor.y + span.advance.y > self.bounds.y1 {
|
||||
if !remaining_text.is_empty() {
|
||||
// Append ellipsis to indicate more content is available, but only if we
|
||||
// haven't already appended a hyphen.
|
||||
let should_append_ellipsis =
|
||||
matches!(self.page_breaking, PageBreaking::CutAndInsertEllipsis)
|
||||
&& !span.insert_hyphen_before_line_break;
|
||||
if should_append_ellipsis {
|
||||
sink.ellipsis(*cursor, self);
|
||||
}
|
||||
// TODO: This does not work in case we are the last
|
||||
// fitting text token on the line, with more text tokens
|
||||
// following and `text.is_empty() == true`.
|
||||
}
|
||||
|
||||
// Report we are out of bounds and quit.
|
||||
sink.out_of_bounds();
|
||||
|
||||
return LayoutFit::OutOfBounds {
|
||||
processed_chars: text.len() - remaining_text.len(),
|
||||
};
|
||||
} else {
|
||||
// Advance the cursor to the beginning of the next line.
|
||||
cursor.x = self.bounds.x0;
|
||||
cursor.y += span.advance.y;
|
||||
|
||||
// Report a line break. While rendering works using the cursor coordinates, we
|
||||
// use explicit line-break reporting in the `Trace` impl.
|
||||
sink.line_break(*cursor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LayoutFit::Fitting {
|
||||
processed_chars: text.len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum LayoutFit {
|
||||
Fitting { processed_chars: usize },
|
||||
OutOfBounds { processed_chars: usize },
|
||||
}
|
||||
|
||||
/// Visitor for text segment operations.
|
||||
pub trait LayoutSink {
|
||||
fn text(&mut self, _cursor: Point, _layout: &TextLayout, _text: &[u8]) {}
|
||||
fn hyphen(&mut self, _cursor: Point, _layout: &TextLayout) {}
|
||||
fn ellipsis(&mut self, _cursor: Point, _layout: &TextLayout) {}
|
||||
fn line_break(&mut self, _cursor: Point) {}
|
||||
fn out_of_bounds(&mut self) {}
|
||||
}
|
||||
|
||||
pub struct TextNoop;
|
||||
|
||||
impl LayoutSink for TextNoop {}
|
||||
|
||||
pub struct TextRenderer;
|
||||
|
||||
impl LayoutSink for TextRenderer {
|
||||
fn text(&mut self, cursor: Point, layout: &TextLayout, text: &[u8]) {
|
||||
display::text(
|
||||
cursor,
|
||||
text,
|
||||
layout.text_font,
|
||||
layout.text_color,
|
||||
layout.background_color,
|
||||
);
|
||||
}
|
||||
|
||||
fn hyphen(&mut self, cursor: Point, layout: &TextLayout) {
|
||||
display::text(
|
||||
cursor,
|
||||
b"-",
|
||||
layout.hyphen_font,
|
||||
layout.hyphen_color,
|
||||
layout.background_color,
|
||||
);
|
||||
}
|
||||
|
||||
fn ellipsis(&mut self, cursor: Point, layout: &TextLayout) {
|
||||
display::text(
|
||||
cursor,
|
||||
b"...",
|
||||
layout.ellipsis_font,
|
||||
layout.ellipsis_color,
|
||||
layout.background_color,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Token<'a> {
|
||||
/// Process literal text content.
|
||||
Literal(&'a [u8]),
|
||||
/// Process argument with specified descriptor.
|
||||
Argument(&'a [u8]),
|
||||
}
|
||||
|
||||
/// Processes a format string into an iterator of `Token`s.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// let parser = Tokenizer::new("Nice to meet {you}, where you been?");
|
||||
/// assert!(matches!(parser.next(), Some(Token::Literal("Nice to meet "))));
|
||||
/// assert!(matches!(parser.next(), Some(Token::Argument("you"))));
|
||||
/// assert!(matches!(parser.next(), Some(Token::Literal(", where you been?"))));
|
||||
/// ```
|
||||
pub struct Tokenizer<'a> {
|
||||
input: &'a [u8],
|
||||
inner: Peekable<Enumerate<slice::Iter<'a, u8>>>,
|
||||
}
|
||||
|
||||
impl<'a> Tokenizer<'a> {
|
||||
/// Create a new tokenizer for bytes of a formatting string `input`,
|
||||
/// returning an iterator.
|
||||
pub fn new(input: &'a [u8]) -> Self {
|
||||
Self {
|
||||
input,
|
||||
inner: input.iter().enumerate().peekable(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Tokenizer<'a> {
|
||||
type Item = Token<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
const ASCII_OPEN_BRACE: u8 = b'{';
|
||||
const ASCII_CLOSED_BRACE: u8 = b'}';
|
||||
|
||||
match self.inner.next() {
|
||||
// Argument token is starting. Read until we find '}', then parse the content between
|
||||
// the braces and return the token. If we encounter the end of string before the closing
|
||||
// brace, quit.
|
||||
Some((open, &ASCII_OPEN_BRACE)) => loop {
|
||||
match self.inner.next() {
|
||||
Some((close, &ASCII_CLOSED_BRACE)) => {
|
||||
break Some(Token::Argument(&self.input[open + 1..close]));
|
||||
}
|
||||
None => {
|
||||
break None;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
// Literal token is starting. Read until we find '{' or the end of string, and return
|
||||
// the token. Use `peek()` for matching the opening brace, se we can keep it
|
||||
// in the iterator for the above code.
|
||||
Some((start, _)) => loop {
|
||||
match self.inner.peek() {
|
||||
Some(&(open, &ASCII_OPEN_BRACE)) => {
|
||||
break Some(Token::Literal(&self.input[start..open]));
|
||||
}
|
||||
None => {
|
||||
break Some(Token::Literal(&self.input[start..]));
|
||||
}
|
||||
_ => {
|
||||
self.inner.next();
|
||||
}
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Op<'a> {
|
||||
/// Render text with current color and font.
|
||||
Text(&'a [u8]),
|
||||
/// Set current text color.
|
||||
Color(Color),
|
||||
/// Set currently used font.
|
||||
Font(Font),
|
||||
}
|
||||
|
||||
impl<'a> Op<'a> {
|
||||
fn skip_n_text_bytes(
|
||||
ops: impl Iterator<Item = Op<'a>>,
|
||||
skip_bytes: usize,
|
||||
) -> impl Iterator<Item = Op<'a>> {
|
||||
let mut skipped = 0;
|
||||
|
||||
ops.filter_map(move |op| match op {
|
||||
Op::Text(text) if skipped < skip_bytes => {
|
||||
skipped = skipped.saturating_add(text.len());
|
||||
if skipped > skip_bytes {
|
||||
let leave_bytes = skipped - skip_bytes;
|
||||
Some(Op::Text(&text[..text.len() - leave_bytes]))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
op_to_pass_through => Some(op_to_pass_through),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct Span {
|
||||
/// How many characters from the input text this span is laying out.
|
||||
length: usize,
|
||||
/// How many chars from the input text should we skip before fitting the
|
||||
/// next span?
|
||||
skip_next_chars: usize,
|
||||
/// By how much to offset the cursor after this span. If the vertical offset
|
||||
/// is bigger than zero, it means we are breaking the line.
|
||||
advance: Offset,
|
||||
/// If we are breaking the line, should we insert a hyphen right after this
|
||||
/// span to indicate a word-break?
|
||||
insert_hyphen_before_line_break: bool,
|
||||
}
|
||||
|
||||
impl Span {
|
||||
fn fit_horizontally(
|
||||
text: &[u8],
|
||||
max_width: i32,
|
||||
text_font: Font,
|
||||
hyphen_font: Font,
|
||||
breaking: LineBreaking,
|
||||
) -> Self {
|
||||
const ASCII_LF: u8 = b'\n';
|
||||
const ASCII_CR: u8 = b'\r';
|
||||
const ASCII_SPACE: u8 = b' ';
|
||||
const ASCII_HYPHEN: u8 = b'-';
|
||||
|
||||
fn is_whitespace(ch: u8) -> bool {
|
||||
ch == ASCII_SPACE || ch == ASCII_LF || ch == ASCII_CR
|
||||
}
|
||||
|
||||
let hyphen_width = hyphen_font.text_width(&[ASCII_HYPHEN]);
|
||||
|
||||
// The span we return in case the line has to break. We mutate it in the
|
||||
// possible break points, and its initial value is returned in case no text
|
||||
// at all is fitting the constraints: zero length, zero width, full line
|
||||
// break.
|
||||
let mut line = Self {
|
||||
length: 0,
|
||||
advance: Offset::new(0, text_font.line_height()),
|
||||
insert_hyphen_before_line_break: false,
|
||||
skip_next_chars: 0,
|
||||
};
|
||||
|
||||
let mut span_width = 0;
|
||||
let mut found_any_whitespace = false;
|
||||
|
||||
for (i, &ch) in text.iter().enumerate() {
|
||||
let char_width = text_font.text_width(&[ch]);
|
||||
|
||||
// Consider if we could be breaking the line at this position.
|
||||
if is_whitespace(ch) {
|
||||
// Break before the whitespace, without hyphen.
|
||||
line.length = i;
|
||||
line.advance.x = span_width;
|
||||
line.insert_hyphen_before_line_break = false;
|
||||
line.skip_next_chars = 1;
|
||||
if ch == ASCII_CR {
|
||||
// We'll be breaking the line, but advancing the cursor only by a half of the
|
||||
// regular line height.
|
||||
line.advance.y = text_font.line_height() / 2;
|
||||
}
|
||||
if ch == ASCII_LF || ch == ASCII_CR {
|
||||
// End of line, break immediately.
|
||||
return line;
|
||||
}
|
||||
found_any_whitespace = true;
|
||||
} else if span_width + char_width > max_width {
|
||||
// Return the last breakpoint.
|
||||
return line;
|
||||
} else {
|
||||
let have_space_for_break = span_width + char_width + hyphen_width <= max_width;
|
||||
let can_break_word = matches!(breaking, LineBreaking::BreakWordsAndInsertHyphen)
|
||||
|| !found_any_whitespace;
|
||||
if have_space_for_break && can_break_word {
|
||||
// Break after this character, append hyphen.
|
||||
line.length = i + 1;
|
||||
line.advance.x = span_width + char_width;
|
||||
line.insert_hyphen_before_line_break = true;
|
||||
line.skip_next_chars = 0;
|
||||
}
|
||||
}
|
||||
|
||||
span_width += char_width;
|
||||
}
|
||||
|
||||
// The whole text is fitting.
|
||||
Self {
|
||||
length: text.len(),
|
||||
advance: Offset::new(span_width, 0),
|
||||
insert_hyphen_before_line_break: false,
|
||||
skip_next_chars: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn tokenizer_yields_expected_tokens() {
|
||||
use std::array::IntoIter;
|
||||
|
||||
assert!(Tokenizer::new(b"").eq(IntoIter::new([])));
|
||||
assert!(Tokenizer::new(b"x").eq(IntoIter::new([Token::Literal(b"x")])));
|
||||
assert!(Tokenizer::new(b"x\0y").eq(IntoIter::new([Token::Literal("x\0y".as_bytes())])));
|
||||
assert!(Tokenizer::new(b"{").eq(IntoIter::new([])));
|
||||
assert!(Tokenizer::new(b"x{").eq(IntoIter::new([Token::Literal(b"x")])));
|
||||
assert!(Tokenizer::new(b"x{y").eq(IntoIter::new([Token::Literal(b"x")])));
|
||||
assert!(Tokenizer::new(b"{}").eq(IntoIter::new([Token::Argument(b"")])));
|
||||
assert!(Tokenizer::new(b"x{}y{").eq(IntoIter::new([
|
||||
Token::Literal(b"x"),
|
||||
Token::Argument(b""),
|
||||
Token::Literal(b"y"),
|
||||
])));
|
||||
assert!(Tokenizer::new(b"{\0}").eq(IntoIter::new([Token::Argument("\0".as_bytes()),])));
|
||||
assert!(Tokenizer::new(b"{{y}").eq(IntoIter::new([Token::Argument(b"{y"),])));
|
||||
assert!(Tokenizer::new(b"{{{{xyz").eq(IntoIter::new([])));
|
||||
assert!(Tokenizer::new(b"x{}{{}}}}").eq(IntoIter::new([
|
||||
Token::Literal(b"x"),
|
||||
Token::Argument(b""),
|
||||
Token::Argument(b"{"),
|
||||
Token::Literal(b"}}}"),
|
||||
])));
|
||||
}
|
||||
}
|
91
core/embed/rust/src/ui/component/model_tt/theme.rs
Normal file
91
core/embed/rust/src/ui/component/model_tt/theme.rs
Normal file
@ -0,0 +1,91 @@
|
||||
use crate::ui::{
|
||||
component::model_tt::{ButtonStyle, ButtonStyleSheet, LabelStyle},
|
||||
display::{Color, Font},
|
||||
};
|
||||
|
||||
// Font constants.
|
||||
pub const FONT_NORMAL: Font = Font::new(-1);
|
||||
pub const FONT_BOLD: Font = Font::new(-2);
|
||||
pub const FONT_MONO: Font = Font::new(-3);
|
||||
|
||||
// Typical backlight values.
|
||||
pub const BACKLIGHT_NORMAL: i32 = 150;
|
||||
pub const BACKLIGHT_LOW: i32 = 45;
|
||||
pub const BACKLIGHT_DIM: i32 = 5;
|
||||
pub const BACKLIGHT_NONE: i32 = 2;
|
||||
pub const BACKLIGHT_MAX: i32 = 255;
|
||||
|
||||
// Color palette.
|
||||
pub const WHITE: Color = Color::rgb(255, 255, 255);
|
||||
pub const BLACK: Color = Color::rgb(0, 0, 0);
|
||||
pub const FG: Color = WHITE; // Default foreground (text & icon) color.
|
||||
pub const BG: Color = BLACK; // Default background color.
|
||||
pub const RED: Color = Color::rgb(205, 73, 73); // dark-coral
|
||||
pub const YELLOW: Color = Color::rgb(193, 144, 9); // ochre
|
||||
pub const GREEN: Color = Color::rgb(57, 168, 20); // grass-green
|
||||
pub const BLUE: Color = Color::rgb(0, 86, 190); // blue
|
||||
pub const GREY_LIGHT: Color = Color::rgb(168, 168, 168); // greyish
|
||||
pub const GREY_DARK: Color = Color::rgb(51, 51, 51); // black
|
||||
|
||||
// Commonly used corner radius (i.e. for buttons).
|
||||
pub const RADIUS: u8 = 4;
|
||||
|
||||
// Size of icons in the UI (i.e. inside buttons).
|
||||
pub const ICON_SIZE: i32 = 16;
|
||||
|
||||
// UI icons.
|
||||
pub const ICON_CANCEL: &[u8] = include_res!("cancel.toif");
|
||||
pub const ICON_CONFIRM: &[u8] = include_res!("confirm.toif");
|
||||
pub const ICON_SPACE: &[u8] = include_res!("space.toif");
|
||||
|
||||
pub fn label_default() -> LabelStyle {
|
||||
LabelStyle {
|
||||
font: FONT_NORMAL,
|
||||
text_color: FG,
|
||||
background_color: BG,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn button_default() -> ButtonStyleSheet {
|
||||
ButtonStyleSheet {
|
||||
normal: &ButtonStyle {
|
||||
font: FONT_NORMAL,
|
||||
text_color: FG,
|
||||
button_color: GREY_DARK,
|
||||
background_color: BG,
|
||||
border_color: BG,
|
||||
border_radius: RADIUS,
|
||||
border_width: 2,
|
||||
},
|
||||
active: &ButtonStyle {
|
||||
font: FONT_NORMAL,
|
||||
text_color: BG,
|
||||
button_color: FG,
|
||||
background_color: BG,
|
||||
border_color: FG,
|
||||
border_radius: RADIUS,
|
||||
border_width: 2,
|
||||
},
|
||||
disabled: &ButtonStyle {
|
||||
font: FONT_NORMAL,
|
||||
text_color: GREY_LIGHT,
|
||||
button_color: GREY_DARK,
|
||||
background_color: BG,
|
||||
border_color: BG,
|
||||
border_radius: RADIUS,
|
||||
border_width: 2,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn button_confirm() -> ButtonStyleSheet {
|
||||
button_default()
|
||||
}
|
||||
|
||||
pub fn button_cancel() -> ButtonStyleSheet {
|
||||
button_default()
|
||||
}
|
||||
|
||||
pub fn button_clear() -> ButtonStyleSheet {
|
||||
button_default()
|
||||
}
|
156
core/embed/rust/src/ui/display.rs
Normal file
156
core/embed/rust/src/ui/display.rs
Normal file
@ -0,0 +1,156 @@
|
||||
use crate::trezorhal::display;
|
||||
|
||||
use super::geometry::{Offset, Point, Rect};
|
||||
|
||||
pub fn width() -> i32 {
|
||||
display::width()
|
||||
}
|
||||
|
||||
pub fn height() -> i32 {
|
||||
display::height()
|
||||
}
|
||||
|
||||
pub fn size() -> Offset {
|
||||
Offset::new(width(), height())
|
||||
}
|
||||
|
||||
pub fn screen() -> Rect {
|
||||
Rect::from_top_left_and_size(Point::zero(), size())
|
||||
}
|
||||
|
||||
pub fn backlight(val: i32) -> i32 {
|
||||
display::backlight(val)
|
||||
}
|
||||
|
||||
pub fn rect(r: Rect, fg_color: Color) {
|
||||
display::bar(r.x0, r.y0, r.width(), r.height(), fg_color.into());
|
||||
}
|
||||
|
||||
pub fn rounded_rect(r: Rect, fg_color: Color, bg_color: Color, radius: u8) {
|
||||
display::bar_radius(
|
||||
r.x0,
|
||||
r.y0,
|
||||
r.width(),
|
||||
r.height(),
|
||||
fg_color.into(),
|
||||
bg_color.into(),
|
||||
radius,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn icon(center: Point, data: &[u8], fg_color: Color, bg_color: Color) {
|
||||
let toif_info = display::toif_info(data).unwrap();
|
||||
assert!(toif_info.grayscale);
|
||||
|
||||
let r = Rect::from_center_and_size(
|
||||
center,
|
||||
Offset::new(toif_info.width as _, toif_info.height as _),
|
||||
);
|
||||
display::icon(
|
||||
r.x0,
|
||||
r.y0,
|
||||
r.width(),
|
||||
r.height(),
|
||||
&data[12..], // skip TOIF header
|
||||
fg_color.into(),
|
||||
bg_color.into(),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn text(baseline: Point, text: &[u8], font: Font, fg_color: Color, bg_color: Color) {
|
||||
display::text(
|
||||
baseline.x,
|
||||
baseline.y,
|
||||
text,
|
||||
font.0,
|
||||
fg_color.into(),
|
||||
bg_color.into(),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn text_width(text: &[u8], font: Font) -> i32 {
|
||||
display::text_width(text, font.0)
|
||||
}
|
||||
|
||||
pub fn text_height() -> i32 {
|
||||
const TEXT_HEIGHT: i32 = 16;
|
||||
TEXT_HEIGHT
|
||||
}
|
||||
|
||||
pub fn line_height() -> i32 {
|
||||
const LINE_HEIGHT: i32 = 26;
|
||||
LINE_HEIGHT
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub struct Font(i32);
|
||||
|
||||
impl Font {
|
||||
pub const fn new(id: i32) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
|
||||
pub fn text_width(self, text: &[u8]) -> i32 {
|
||||
text_width(text, self)
|
||||
}
|
||||
|
||||
pub fn line_height(self) -> i32 {
|
||||
line_height()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub struct Color(u16);
|
||||
|
||||
impl Color {
|
||||
pub const fn from_u16(val: u16) -> Self {
|
||||
Self(val)
|
||||
}
|
||||
|
||||
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
|
||||
let r = (r as u16 & 0xF8) << 8;
|
||||
let g = (g as u16 & 0xFC) << 3;
|
||||
let b = (b as u16 & 0xF8) >> 3;
|
||||
Self(r | g | b)
|
||||
}
|
||||
|
||||
pub const fn r(self) -> u8 {
|
||||
(self.0 >> 8) as u8 & 0xF8
|
||||
}
|
||||
|
||||
pub const fn g(self) -> u8 {
|
||||
(self.0 >> 3) as u8 & 0xFC
|
||||
}
|
||||
|
||||
pub const fn b(self) -> u8 {
|
||||
(self.0 << 3) as u8 & 0xF8
|
||||
}
|
||||
|
||||
pub fn blend(self, other: Self, t: f32) -> Self {
|
||||
Self::rgb(
|
||||
lerp(self.r(), other.r(), t),
|
||||
lerp(self.g(), other.g(), t),
|
||||
lerp(self.b(), other.b(), t),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn to_u16(self) -> u16 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u16> for Color {
|
||||
fn from(val: u16) -> Self {
|
||||
Self(val)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for u16 {
|
||||
fn from(val: Color) -> Self {
|
||||
val.to_u16()
|
||||
}
|
||||
}
|
||||
|
||||
fn lerp(a: u8, b: u8, t: f32) -> u8 {
|
||||
(a as f32 + t * (b - a) as f32) as u8
|
||||
}
|
229
core/embed/rust/src/ui/geometry.rs
Normal file
229
core/embed/rust/src/ui/geometry.rs
Normal file
@ -0,0 +1,229 @@
|
||||
use core::ops::{Add, Sub};
|
||||
|
||||
/// Relative offset in 2D space, used for representing translation and
|
||||
/// dimensions of objects. Absolute positions on the screen are represented by
|
||||
/// the `Point` type.
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub struct Offset {
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
}
|
||||
|
||||
impl Offset {
|
||||
pub const fn new(x: i32, y: i32) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
|
||||
pub const fn uniform(a: i32) -> Self {
|
||||
Self::new(a, a)
|
||||
}
|
||||
|
||||
pub const fn zero() -> Self {
|
||||
Self::new(0, 0)
|
||||
}
|
||||
|
||||
pub fn abs(self) -> Self {
|
||||
Self::new(self.x.abs(), self.y.abs())
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<Offset> for Offset {
|
||||
type Output = Offset;
|
||||
|
||||
fn add(self, rhs: Offset) -> Self::Output {
|
||||
Self::new(self.x + rhs.x, self.y + rhs.y)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<Offset> for Offset {
|
||||
type Output = Offset;
|
||||
|
||||
fn sub(self, rhs: Offset) -> Self::Output {
|
||||
Self::new(self.x - rhs.x, self.y - rhs.y)
|
||||
}
|
||||
}
|
||||
|
||||
/// A point in 2D space defined by the the `x` and `y` coordinate. Relative
|
||||
/// coordinates, vectors, and offsets are represented by the `Offset` type.
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub struct Point {
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
}
|
||||
|
||||
impl Point {
|
||||
pub const fn new(x: i32, y: i32) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
|
||||
pub const fn zero() -> Self {
|
||||
Self::new(0, 0)
|
||||
}
|
||||
|
||||
pub fn center(self, rhs: Self) -> Self {
|
||||
Self::new((self.x + rhs.x) / 2, (self.y + rhs.y) / 2)
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<Offset> for Point {
|
||||
type Output = Point;
|
||||
|
||||
fn add(self, rhs: Offset) -> Self::Output {
|
||||
Self::new(self.x + rhs.x, self.y + rhs.y)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<Offset> for Point {
|
||||
type Output = Point;
|
||||
|
||||
fn sub(self, rhs: Offset) -> Self::Output {
|
||||
Self::new(self.x - rhs.x, self.y - rhs.y)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<Point> for Point {
|
||||
type Output = Offset;
|
||||
|
||||
fn sub(self, rhs: Point) -> Self::Output {
|
||||
Offset::new(self.x - rhs.x, self.y - rhs.y)
|
||||
}
|
||||
}
|
||||
|
||||
/// A rectangle in 2D space defined by the top-left point `x0`,`y0` and the
|
||||
/// bottom-right point `x1`,`y1`.
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub struct Rect {
|
||||
pub x0: i32,
|
||||
pub y0: i32,
|
||||
pub x1: i32,
|
||||
pub y1: i32,
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
pub const fn new(p0: Point, p1: Point) -> Self {
|
||||
Self {
|
||||
x0: p0.x,
|
||||
y0: p0.y,
|
||||
x1: p1.x,
|
||||
y1: p1.y,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_top_left_and_size(p0: Point, size: Offset) -> Self {
|
||||
Self::new(p0, p0 + size)
|
||||
}
|
||||
|
||||
pub fn from_center_and_size(p: Point, size: Offset) -> Self {
|
||||
Self {
|
||||
x0: p.x - size.x / 2,
|
||||
y0: p.y - size.y / 2,
|
||||
x1: p.x + size.x / 2,
|
||||
y1: p.y + size.y / 2,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn width(&self) -> i32 {
|
||||
self.x1 - self.x0
|
||||
}
|
||||
|
||||
pub fn height(&self) -> i32 {
|
||||
self.y1 - self.y0
|
||||
}
|
||||
|
||||
pub fn top_left(&self) -> Point {
|
||||
Point::new(self.x0, self.y0)
|
||||
}
|
||||
|
||||
pub fn top_right(&self) -> Point {
|
||||
Point::new(self.x1, self.y0)
|
||||
}
|
||||
|
||||
pub fn bottom_left(&self) -> Point {
|
||||
Point::new(self.x0, self.y1)
|
||||
}
|
||||
|
||||
pub fn bottom_right(&self) -> Point {
|
||||
Point::new(self.x1, self.y1)
|
||||
}
|
||||
|
||||
pub fn center(&self) -> Point {
|
||||
self.top_left().center(self.bottom_right())
|
||||
}
|
||||
|
||||
pub fn contains(&self, point: Point) -> bool {
|
||||
point.x >= self.x0 && point.x < self.x1 && point.y >= self.y0 && point.y < self.y1
|
||||
}
|
||||
|
||||
pub fn inset(&self, uniform: i32) -> Self {
|
||||
Self {
|
||||
x0: self.x0 + uniform,
|
||||
y0: self.y0 + uniform,
|
||||
x1: self.x1 - uniform,
|
||||
y1: self.y1 - uniform,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cut_from_left(&self, width: i32) -> Self {
|
||||
Self {
|
||||
x0: self.x0,
|
||||
y0: self.y0,
|
||||
x1: self.x0 + width,
|
||||
y1: self.y1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cut_from_right(&self, width: i32) -> Self {
|
||||
Self {
|
||||
x0: self.x1 - width,
|
||||
y0: self.y0,
|
||||
x1: self.x1,
|
||||
y1: self.y1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Align {
|
||||
Left,
|
||||
Right,
|
||||
Center,
|
||||
}
|
||||
|
||||
pub struct Grid {
|
||||
/// Number of rows (cells on the y-axis) in the grid.
|
||||
pub rows: usize,
|
||||
/// Number of columns (cells on the x-axis) in the grid.
|
||||
pub cols: usize,
|
||||
/// Padding between cells.
|
||||
pub spacing: i32,
|
||||
/// Total area covered by this grid.
|
||||
pub area: Rect,
|
||||
}
|
||||
|
||||
impl Grid {
|
||||
pub fn new(area: Rect, rows: usize, cols: usize) -> Self {
|
||||
Self {
|
||||
rows,
|
||||
cols,
|
||||
spacing: 0,
|
||||
area,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn row_col(&self, row: usize, col: usize) -> Rect {
|
||||
let cell_width = self.area.width() / self.cols as i32;
|
||||
let cell_height = self.area.height() / self.rows as i32;
|
||||
let x = col as i32 * cell_width;
|
||||
let y = row as i32 * cell_height;
|
||||
Rect {
|
||||
x0: self.area.x0 + x,
|
||||
y0: self.area.y0 + y,
|
||||
x1: self.area.x0 + x + (cell_width - self.spacing),
|
||||
y1: self.area.y0 + y + (cell_height - self.spacing),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cell(&self, index: usize) -> Rect {
|
||||
self.row_col(index / self.cols, index % self.cols)
|
||||
}
|
||||
}
|
117
core/embed/rust/src/ui/layout/example.rs
Normal file
117
core/embed/rust/src/ui/layout/example.rs
Normal file
@ -0,0 +1,117 @@
|
||||
use core::convert::{TryFrom, TryInto};
|
||||
|
||||
use crate::{
|
||||
error::Error,
|
||||
micropython::{buffer::Buffer, obj::Obj},
|
||||
ui::{
|
||||
component::{
|
||||
model_tt::{theme, Button, Dialog, DialogMsg, Text},
|
||||
Child,
|
||||
},
|
||||
display,
|
||||
},
|
||||
util,
|
||||
};
|
||||
|
||||
use super::obj::LayoutObj;
|
||||
|
||||
impl<T> TryFrom<DialogMsg<T>> for Obj
|
||||
where
|
||||
Obj: TryFrom<T>,
|
||||
Error: From<<T as TryInto<Obj>>::Error>,
|
||||
{
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(val: DialogMsg<T>) -> Result<Self, Self::Error> {
|
||||
match val {
|
||||
DialogMsg::Content(c) => Ok(c.try_into()?),
|
||||
DialogMsg::LeftClicked => 1.try_into(),
|
||||
DialogMsg::RightClicked => 2.try_into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
extern "C" fn ui_layout_new_example(param: Obj) -> Obj {
|
||||
let block = move || {
|
||||
let param: Buffer = param.try_into()?;
|
||||
let layout = LayoutObj::new(Child::new(Dialog::new(
|
||||
display::screen(),
|
||||
|area| {
|
||||
Text::new(area, param)
|
||||
.with(b"some", "a few")
|
||||
.with(b"param", "xx")
|
||||
},
|
||||
|area| Button::with_text(area, b"Left", theme::button_default()),
|
||||
|area| Button::with_text(area, b"Right", theme::button_default()),
|
||||
)))?;
|
||||
Ok(layout.into())
|
||||
};
|
||||
unsafe { util::try_or_raise(block) }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::trace::{Trace, Tracer};
|
||||
|
||||
use super::*;
|
||||
|
||||
impl Tracer for Vec<u8> {
|
||||
fn bytes(&mut self, b: &[u8]) {
|
||||
self.extend(b)
|
||||
}
|
||||
|
||||
fn string(&mut self, s: &str) {
|
||||
self.extend(s.as_bytes())
|
||||
}
|
||||
|
||||
fn symbol(&mut self, name: &str) {
|
||||
self.extend(name.as_bytes())
|
||||
}
|
||||
|
||||
fn open(&mut self, name: &str) {
|
||||
self.extend(b"<");
|
||||
self.extend(name.as_bytes());
|
||||
self.extend(b" ");
|
||||
}
|
||||
|
||||
fn field(&mut self, name: &str, value: &dyn Trace) {
|
||||
self.extend(name.as_bytes());
|
||||
self.extend(b":");
|
||||
value.trace(self);
|
||||
self.extend(b" ");
|
||||
}
|
||||
|
||||
fn close(&mut self) {
|
||||
self.extend(b">")
|
||||
}
|
||||
}
|
||||
|
||||
fn trace(val: &impl Trace) -> String {
|
||||
let mut t = Vec::new();
|
||||
val.trace(&mut t);
|
||||
String::from_utf8(t).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trace_example_layout() {
|
||||
let layout = Child::new(Dialog::new(
|
||||
display::screen(),
|
||||
|area| {
|
||||
Text::new(
|
||||
area,
|
||||
"Testing text layout, with some text, and some more text. And {param}",
|
||||
)
|
||||
.with(b"param", b"parameters!")
|
||||
},
|
||||
|area| Button::with_text(area, b"Left", theme::button_default()),
|
||||
|area| Button::with_text(area, b"Right", theme::button_default()),
|
||||
));
|
||||
assert_eq!(
|
||||
trace(&layout),
|
||||
r#"<Dialog content:<Text content:Testing text layout, with
|
||||
some text, and some more
|
||||
text. And parameters! > left:<Button text:Left > right:<Button text:Right > >"#
|
||||
)
|
||||
}
|
||||
}
|
2
core/embed/rust/src/ui/layout/mod.rs
Normal file
2
core/embed/rust/src/ui/layout/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
mod example;
|
||||
mod obj;
|
357
core/embed/rust/src/ui/layout/obj.rs
Normal file
357
core/embed/rust/src/ui/layout/obj.rs
Normal file
@ -0,0 +1,357 @@
|
||||
use core::{
|
||||
cell::RefCell,
|
||||
convert::{TryFrom, TryInto},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::Error,
|
||||
micropython::{
|
||||
gc::Gc,
|
||||
map::Map,
|
||||
obj::{Obj, ObjBase},
|
||||
qstr::Qstr,
|
||||
typ::Type,
|
||||
},
|
||||
ui::{
|
||||
component::{Child, Component, Event, EventCtx, Never, TimerToken},
|
||||
geometry::Point,
|
||||
},
|
||||
util,
|
||||
};
|
||||
|
||||
/// Conversion trait implemented by components that know how to convert their
|
||||
/// message values into MicroPython `Obj`s. We can automatically implement
|
||||
/// `ComponentMsgObj` for components whose message types implement `TryInto`.
|
||||
pub trait ComponentMsgObj: Component {
|
||||
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error>;
|
||||
}
|
||||
|
||||
impl<T> ComponentMsgObj for T
|
||||
where
|
||||
T: Component,
|
||||
T::Msg: TryInto<Obj, Error = Error>,
|
||||
{
|
||||
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
|
||||
msg.try_into()
|
||||
}
|
||||
}
|
||||
|
||||
/// In order to store any type of component in a layout, we need to access it
|
||||
/// through an object-safe trait. `Component` itself is not object-safe because
|
||||
/// of `Component::Msg` associated type. `ObjComponent` is a simple object-safe
|
||||
/// wrapping trait that is implemented for all components where `Component::Msg`
|
||||
/// can be converted to `Obj` through the `ComponentMsgObj` trait.
|
||||
pub trait ObjComponent {
|
||||
fn obj_event(&mut self, ctx: &mut EventCtx, event: Event) -> Result<Obj, Error>;
|
||||
fn obj_paint(&mut self);
|
||||
}
|
||||
|
||||
impl<T> ObjComponent for Child<T>
|
||||
where
|
||||
T: ComponentMsgObj,
|
||||
{
|
||||
fn obj_event(&mut self, ctx: &mut EventCtx, event: Event) -> Result<Obj, Error> {
|
||||
if let Some(msg) = self.event(ctx, event) {
|
||||
self.inner().msg_try_into_obj(msg)
|
||||
} else {
|
||||
Ok(Obj::const_none())
|
||||
}
|
||||
}
|
||||
|
||||
fn obj_paint(&mut self) {
|
||||
self.paint();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
mod maybe_trace {
|
||||
pub trait ObjComponentTrace: super::ObjComponent + crate::trace::Trace {}
|
||||
impl<T> ObjComponentTrace for T where T: super::ObjComponent + crate::trace::Trace {}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ui_debug"))]
|
||||
mod maybe_trace {
|
||||
pub trait ObjComponentTrace: super::ObjComponent {}
|
||||
impl<T> ObjComponentTrace for T where T: super::ObjComponent {}
|
||||
}
|
||||
|
||||
/// Trait that combines `ObjComponent` with `Trace` if `ui_debug` is enabled,
|
||||
/// and pure `ObjComponent` otherwise.
|
||||
use maybe_trace::ObjComponentTrace;
|
||||
|
||||
/// `LayoutObj` is a GC-allocated object exported to MicroPython, with type
|
||||
/// `LayoutObj::obj_type()`. It wraps a root component through the
|
||||
/// `ObjComponent` trait.
|
||||
#[repr(C)]
|
||||
pub struct LayoutObj {
|
||||
base: ObjBase,
|
||||
inner: RefCell<LayoutObjInner>,
|
||||
}
|
||||
|
||||
struct LayoutObjInner {
|
||||
root: Gc<dyn ObjComponentTrace>,
|
||||
event_ctx: EventCtx,
|
||||
timer_fn: Obj,
|
||||
}
|
||||
|
||||
impl LayoutObj {
|
||||
/// Create a new `LayoutObj`, wrapping a root component.
|
||||
pub fn new(root: impl ObjComponentTrace + 'static) -> Result<Gc<Self>, Error> {
|
||||
// SAFETY: We are coercing GC-allocated sized ptr into an unsized one.
|
||||
let root =
|
||||
unsafe { Gc::from_raw(Gc::into_raw(Gc::new(root)?) as *mut dyn ObjComponentTrace) };
|
||||
|
||||
Gc::new(Self {
|
||||
base: Self::obj_type().as_base(),
|
||||
inner: RefCell::new(LayoutObjInner {
|
||||
root,
|
||||
event_ctx: EventCtx::new(),
|
||||
timer_fn: Obj::const_none(),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
/// Timer callback is expected to be a callable object of the following
|
||||
/// form: `def timer(token: int, deadline_in_ms: int)`.
|
||||
fn obj_set_timer_fn(&self, timer_fn: Obj) {
|
||||
self.inner.borrow_mut().timer_fn = timer_fn;
|
||||
}
|
||||
|
||||
/// Run an event pass over the component tree. After the traversal, any
|
||||
/// pending timers are drained into `self.timer_callback`. Returns `Err`
|
||||
/// in case the timer callback raises or one of the components returns
|
||||
/// an error, `Ok` with the message otherwise.
|
||||
fn obj_event(&self, event: Event) -> Result<Obj, Error> {
|
||||
let inner = &mut *self.inner.borrow_mut();
|
||||
|
||||
// Clear the upwards-propagating paint request flag from the last event pass.
|
||||
inner.event_ctx.clear_paint_requests();
|
||||
|
||||
// Send the event down the component tree. Bail out in case of failure.
|
||||
// SAFETY: `inner.root` is unique because of the `inner.borrow_mut()`.
|
||||
let msg = unsafe { Gc::as_mut(&mut inner.root) }.obj_event(&mut inner.event_ctx, event)?;
|
||||
|
||||
// All concerning `Child` wrappers should have already marked themselves for
|
||||
// painting by now, and we're prepared for a paint pass.
|
||||
|
||||
// Drain any pending timers into the callback.
|
||||
while let Some((token, deadline)) = inner.event_ctx.pop_timer() {
|
||||
let token = token.try_into();
|
||||
let deadline = deadline.try_into();
|
||||
if let (Ok(token), Ok(deadline)) = (token, deadline) {
|
||||
inner.timer_fn.call_with_n_args(&[token, deadline])?;
|
||||
} else {
|
||||
// Failed to convert token or deadline into `Obj`, skip.
|
||||
}
|
||||
}
|
||||
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
/// Run a paint pass over the component tree.
|
||||
fn obj_paint_if_requested(&self) {
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
// SAFETY: `inner.root` is unique because of the `inner.borrow_mut()`.
|
||||
unsafe { Gc::as_mut(&mut inner.root) }.obj_paint();
|
||||
}
|
||||
|
||||
/// Run a tracing pass over the component tree. Passed `callback` is called
|
||||
/// with each piece of tracing information. Panics in case the callback
|
||||
/// raises an exception.
|
||||
#[cfg(feature = "ui_debug")]
|
||||
fn obj_trace(&self, callback: Obj) {
|
||||
use crate::trace::{Trace, Tracer};
|
||||
|
||||
struct CallbackTracer(Obj);
|
||||
|
||||
impl Tracer for CallbackTracer {
|
||||
fn bytes(&mut self, b: &[u8]) {
|
||||
self.0.call_with_n_args(&[b.try_into().unwrap()]).unwrap();
|
||||
}
|
||||
|
||||
fn string(&mut self, s: &str) {
|
||||
self.0.call_with_n_args(&[s.try_into().unwrap()]).unwrap();
|
||||
}
|
||||
|
||||
fn symbol(&mut self, name: &str) {
|
||||
self.0
|
||||
.call_with_n_args(&[name.try_into().unwrap()])
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn open(&mut self, name: &str) {
|
||||
self.0
|
||||
.call_with_n_args(&[name.try_into().unwrap()])
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn field(&mut self, name: &str, value: &dyn Trace) {
|
||||
self.0
|
||||
.call_with_n_args(&[name.try_into().unwrap()])
|
||||
.unwrap();
|
||||
value.trace(self);
|
||||
}
|
||||
|
||||
fn close(&mut self) {}
|
||||
}
|
||||
|
||||
self.inner
|
||||
.borrow()
|
||||
.root
|
||||
.trace(&mut CallbackTracer(callback));
|
||||
}
|
||||
|
||||
fn obj_type() -> &'static Type {
|
||||
static TYPE: Type = obj_type! {
|
||||
name: Qstr::MP_QSTR_Layout,
|
||||
locals: &obj_dict!(obj_map! {
|
||||
Qstr::MP_QSTR_set_timer_fn => obj_fn_2!(ui_layout_set_timer_fn).as_obj(),
|
||||
Qstr::MP_QSTR_touch_start => obj_fn_3!(ui_layout_touch_start).as_obj(),
|
||||
Qstr::MP_QSTR_touch_move => obj_fn_3!(ui_layout_touch_move).as_obj(),
|
||||
Qstr::MP_QSTR_touch_end => obj_fn_3!(ui_layout_touch_end).as_obj(),
|
||||
Qstr::MP_QSTR_timer => obj_fn_2!(ui_layout_timer).as_obj(),
|
||||
Qstr::MP_QSTR_paint => obj_fn_1!(ui_layout_paint).as_obj(),
|
||||
Qstr::MP_QSTR_trace => obj_fn_2!(ui_layout_trace).as_obj(),
|
||||
}),
|
||||
};
|
||||
&TYPE
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Gc<LayoutObj>> for Obj {
|
||||
fn from(val: Gc<LayoutObj>) -> Self {
|
||||
// SAFETY:
|
||||
// - We are GC-allocated.
|
||||
// - We are `repr(C)`.
|
||||
// - We have a `base` as the first field with the correct type.
|
||||
unsafe { Obj::from_ptr(Gc::into_raw(val).cast()) }
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Obj> for Gc<LayoutObj> {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: Obj) -> Result<Self, Self::Error> {
|
||||
if LayoutObj::obj_type().is_type_of(value) {
|
||||
// SAFETY: We assume that if `value` is an object pointer with the correct type,
|
||||
// it is always GC-allocated.
|
||||
let this = unsafe { Gc::from_raw(value.as_ptr().cast()) };
|
||||
Ok(this)
|
||||
} else {
|
||||
Err(Error::TypeError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Obj> for TimerToken {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: Obj) -> Result<Self, Self::Error> {
|
||||
let raw: usize = value.try_into()?;
|
||||
let this = Self::from_raw(raw);
|
||||
Ok(this)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<TimerToken> for Obj {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: TimerToken) -> Result<Self, Self::Error> {
|
||||
value.into_raw().try_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Duration> for Obj {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: Duration) -> Result<Self, Self::Error> {
|
||||
let millis: usize = value.as_millis().try_into()?;
|
||||
millis.try_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Never> for Obj {
|
||||
fn from(_: Never) -> Self {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn ui_layout_set_timer_fn(this: Obj, timer_fn: Obj) -> Obj {
|
||||
let block = || {
|
||||
let this: Gc<LayoutObj> = this.try_into()?;
|
||||
this.obj_set_timer_fn(timer_fn);
|
||||
Ok(Obj::const_true())
|
||||
};
|
||||
unsafe { util::try_or_raise(block) }
|
||||
}
|
||||
|
||||
extern "C" fn ui_layout_touch_start(this: Obj, x: Obj, y: Obj) -> Obj {
|
||||
let block = || {
|
||||
let this: Gc<LayoutObj> = this.try_into()?;
|
||||
let event = Event::TouchStart(Point::new(x.try_into()?, y.try_into()?));
|
||||
let msg = this.obj_event(event)?;
|
||||
Ok(msg)
|
||||
};
|
||||
unsafe { util::try_or_raise(block) }
|
||||
}
|
||||
|
||||
extern "C" fn ui_layout_touch_move(this: Obj, x: Obj, y: Obj) -> Obj {
|
||||
let block = || {
|
||||
let this: Gc<LayoutObj> = this.try_into()?;
|
||||
let event = Event::TouchMove(Point::new(x.try_into()?, y.try_into()?));
|
||||
let msg = this.obj_event(event)?;
|
||||
Ok(msg)
|
||||
};
|
||||
unsafe { util::try_or_raise(block) }
|
||||
}
|
||||
|
||||
extern "C" fn ui_layout_touch_end(this: Obj, x: Obj, y: Obj) -> Obj {
|
||||
let block = || {
|
||||
let this: Gc<LayoutObj> = this.try_into()?;
|
||||
let event = Event::TouchEnd(Point::new(x.try_into()?, y.try_into()?));
|
||||
let msg = this.obj_event(event)?;
|
||||
Ok(msg)
|
||||
};
|
||||
unsafe { util::try_or_raise(block) }
|
||||
}
|
||||
|
||||
extern "C" fn ui_layout_timer(this: Obj, token: Obj) -> Obj {
|
||||
let block = || {
|
||||
let this: Gc<LayoutObj> = this.try_into()?;
|
||||
let event = Event::Timer(token.try_into()?);
|
||||
let msg = this.obj_event(event)?;
|
||||
Ok(msg)
|
||||
};
|
||||
unsafe { util::try_or_raise(block) }
|
||||
}
|
||||
|
||||
extern "C" fn ui_layout_paint(this: Obj) -> Obj {
|
||||
let block = || {
|
||||
let this: Gc<LayoutObj> = this.try_into()?;
|
||||
this.obj_paint_if_requested();
|
||||
Ok(Obj::const_true())
|
||||
};
|
||||
unsafe { util::try_or_raise(block) }
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ui_debug_layout_type() -> &'static Type {
|
||||
LayoutObj::obj_type()
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
extern "C" fn ui_layout_trace(this: Obj, callback: Obj) -> Obj {
|
||||
let block = || {
|
||||
let this: Gc<LayoutObj> = this.try_into()?;
|
||||
this.obj_trace(callback);
|
||||
Ok(Obj::const_none())
|
||||
};
|
||||
unsafe { util::try_or_raise(block) }
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ui_debug"))]
|
||||
extern "C" fn ui_layout_trace(_this: Obj, _callback: Obj) -> Obj {
|
||||
Obj::const_none()
|
||||
}
|
9
core/embed/rust/src/ui/macros.rs
Normal file
9
core/embed/rust/src/ui/macros.rs
Normal file
@ -0,0 +1,9 @@
|
||||
macro_rules! include_res {
|
||||
($filename:expr) => {
|
||||
include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../../src/trezor/res/",
|
||||
$filename,
|
||||
))
|
||||
};
|
||||
}
|
7
core/embed/rust/src/ui/mod.rs
Normal file
7
core/embed/rust/src/ui/mod.rs
Normal file
@ -0,0 +1,7 @@
|
||||
#[macro_use]
|
||||
pub mod macros;
|
||||
|
||||
pub mod component;
|
||||
pub mod display;
|
||||
pub mod geometry;
|
||||
pub mod layout;
|
@ -196,6 +196,7 @@ extern const struct _mp_print_t mp_stderr_print;
|
||||
#define MICROPY_PY_TREZORUI (1)
|
||||
#define MICROPY_PY_TREZORUTILS (1)
|
||||
#define MICROPY_PY_TREZORPROTO (1)
|
||||
#define MICROPY_PY_TREZORUI2 (1)
|
||||
|
||||
#define MP_STATE_PORT MP_STATE_VM
|
||||
|
||||
|
6
core/mocks/generated/trezorui2.pyi
Normal file
6
core/mocks/generated/trezorui2.pyi
Normal file
@ -0,0 +1,6 @@
|
||||
from typing import *
|
||||
|
||||
|
||||
# extmod/rustmods/modtrezorui2.c
|
||||
def layout_new_example(text: str) -> None:
|
||||
"""Example layout."""
|
@ -439,3 +439,28 @@ class Layout(Component):
|
||||
def wait_until_layout_is_running() -> Awaitable[None]: # type: ignore
|
||||
while not layout_chan.takers:
|
||||
yield
|
||||
|
||||
|
||||
if utils.MODEL == "1":
|
||||
|
||||
class RustLayout(Layout):
|
||||
def __init__(self, layout: Any):
|
||||
self.layout = layout
|
||||
self.layout.set_timer_fn(self.set_timer)
|
||||
|
||||
def set_timer(self, token: int, deadline: int) -> None:
|
||||
# TODO: schedule a timer tick with `token` in `deadline` ms
|
||||
print("timer", token, deadline)
|
||||
|
||||
def dispatch(self, event: int, x: int, y: int) -> None:
|
||||
msg = None
|
||||
if event is RENDER:
|
||||
self.layout.paint()
|
||||
elif event is io.TOUCH_START:
|
||||
msg = self.layout.touch_start(x, y)
|
||||
elif event is io.TOUCH_MOVE:
|
||||
msg = self.layout.touch_move(x, y)
|
||||
elif event is io.TOUCH_END:
|
||||
msg = self.layout.touch_end(x, y)
|
||||
if msg is not None:
|
||||
raise Result(msg)
|
||||
|
@ -194,6 +194,8 @@ types = {
|
||||
"qstrdata": "q",
|
||||
"protomsg": "P",
|
||||
"protodef": "p",
|
||||
"uilayout": "U",
|
||||
"uilayoutinner": "u",
|
||||
}
|
||||
|
||||
pixels_per_line = len(
|
||||
|
Loading…
Reference in New Issue
Block a user