feat(core): add Rust UI components, layouts, text rendering

[no changelog]
pull/1844/head
Jan Pochyla 3 years ago committed by Martin Milata
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");

@ -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)

@ -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 {

@ -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(())
}
}

@ -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
}
}

@ -0,0 +1,5 @@
mod base;
pub mod model_t1;
pub mod model_tt;
pub use base::{Child, Component, Event, EventCtx, Never, TimerToken};

@ -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,
}

@ -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();
}
}

@ -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) {}
}

@ -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,
);
}
}

@ -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};

@ -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;
}
}
}

@ -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,
);
}
}

@ -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,
);
}
}
}

@ -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) {}
}

@ -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"}}}"),
])));
}
}

@ -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()
}

@ -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
}

@ -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)
}
}

@ -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 > >"#
)
}
}

@ -0,0 +1,2 @@
mod example;
mod obj;

@ -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()
}

@ -0,0 +1,9 @@
macro_rules! include_res {
($filename:expr) => {
include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../src/trezor/res/",
$filename,
))
};
}

@ -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

@ -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…
Cancel
Save