mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-01-08 14:31:06 +00:00
feat(core/rust): JSON output from component tracing
This commit is contained in:
parent
b63b72ed90
commit
eee4c624f9
30
core/embed/rust/Cargo.lock
generated
30
core/embed/rust/Cargo.lock
generated
@ -113,6 +113,12 @@ dependencies = [
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
@ -241,12 +247,35 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.160"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c"
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.96"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.1.0"
|
||||
@ -292,6 +321,7 @@ dependencies = [
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"qrcodegen",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -88,3 +88,6 @@ version = "1.0.69"
|
||||
[build-dependencies.glob]
|
||||
optional = true
|
||||
version = "0.3.0"
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = "1.0.96"
|
||||
|
@ -13,6 +13,7 @@ mod error;
|
||||
// use trezorhal for its macros early
|
||||
#[macro_use]
|
||||
mod trezorhal;
|
||||
mod maybe_trace;
|
||||
#[cfg(feature = "micropython")]
|
||||
#[macro_use]
|
||||
mod micropython;
|
||||
|
15
core/embed/rust/src/maybe_trace.rs
Normal file
15
core/embed/rust/src/maybe_trace.rs
Normal file
@ -0,0 +1,15 @@
|
||||
#[cfg(feature = "ui_debug")]
|
||||
mod maybe_trace {
|
||||
use crate::trace::Trace;
|
||||
|
||||
pub trait MaybeTrace: Trace {}
|
||||
impl<T> MaybeTrace for T where T: Trace {}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ui_debug"))]
|
||||
mod maybe_trace {
|
||||
pub trait MaybeTrace {}
|
||||
impl<T> MaybeTrace for T {}
|
||||
}
|
||||
|
||||
pub use maybe_trace::MaybeTrace;
|
@ -253,10 +253,3 @@ pub fn hexlify_bytes(obj: Obj, offset: usize, max_len: usize) -> Result<StrBuffe
|
||||
let result = StrBuffer::alloc_with(hex_len, move |buffer| hexlify(bin_slice, buffer))?;
|
||||
Ok(result.offset(hex_off))
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for StrBuffer {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
self.as_ref().trace(t)
|
||||
}
|
||||
}
|
||||
|
@ -188,9 +188,15 @@ mod tests {
|
||||
let list = unsafe { Gc::as_mut(&mut gc_list) };
|
||||
|
||||
for (i, value) in vec.iter().copied().enumerate() {
|
||||
assert_eq!(value, list.get(i).unwrap().try_into().unwrap());
|
||||
assert_eq!(
|
||||
value,
|
||||
TryInto::<u16>::try_into(list.get(i).unwrap()).unwrap()
|
||||
);
|
||||
list.set(i, Obj::from(value + 1)).unwrap();
|
||||
assert_eq!(value + 1, list.get(i).unwrap().try_into().unwrap());
|
||||
assert_eq!(
|
||||
value + 1,
|
||||
TryInto::<u16>::try_into(list.get(i).unwrap()).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
let mut buf = IterBuf::new();
|
||||
@ -215,7 +221,7 @@ mod tests {
|
||||
let slice = unsafe { list.as_slice() };
|
||||
assert_eq!(slice.len(), vec.len());
|
||||
for i in 0..slice.len() {
|
||||
assert_eq!(vec[i], slice[i].try_into().unwrap());
|
||||
assert_eq!(vec[i], TryInto::<u16>::try_into(slice[i]).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
@ -228,7 +234,7 @@ mod tests {
|
||||
|
||||
let slice = unsafe { Gc::as_mut(&mut list).as_mut_slice() };
|
||||
assert_eq!(slice.len(), vec.len());
|
||||
assert_eq!(vec[0], slice[0].try_into().unwrap());
|
||||
assert_eq!(vec[0], TryInto::<u16>::try_into(slice[0]).unwrap());
|
||||
|
||||
for i in 0..slice.len() {
|
||||
slice[i] = ((i + 10) as u16).into();
|
||||
|
@ -9,3 +9,34 @@ pub fn hexlify(data: &[u8], buffer: &mut [u8]) {
|
||||
i += 2;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format_i64(num: i64, buffer: &mut [u8]) -> Option<&str> {
|
||||
let mut i = 0;
|
||||
let mut num = num;
|
||||
let negative = if num < 0 {
|
||||
num = -num;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
while num > 0 && i < buffer.len() {
|
||||
buffer[i] = b'0' + ((num % 10) as u8);
|
||||
num /= 10;
|
||||
i += 1;
|
||||
}
|
||||
match i {
|
||||
0 => Some("0"),
|
||||
_ if num > 0 => None,
|
||||
_ if negative && i == buffer.len() => None,
|
||||
_ => {
|
||||
if negative {
|
||||
buffer[i] = b'-';
|
||||
i += 1;
|
||||
}
|
||||
let result = &mut buffer[..i];
|
||||
result.reverse();
|
||||
Some(unsafe { core::str::from_utf8_unchecked(result) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,91 +1,212 @@
|
||||
/// Visitor passed into `Trace` types.
|
||||
use crate::strutil::format_i64;
|
||||
|
||||
pub trait Tracer {
|
||||
fn child(&mut self, key: &str, value: &dyn Trace);
|
||||
fn int(&mut self, key: &str, i: i64);
|
||||
fn string(&mut self, key: &str, s: &str);
|
||||
fn bool(&mut self, key: &str, b: bool);
|
||||
|
||||
fn in_child(&mut self, key: &str, block: &dyn Fn(&mut dyn Tracer));
|
||||
fn in_list(&mut self, key: &str, block: &dyn Fn(&mut dyn ListTracer));
|
||||
|
||||
fn component(&mut self, name: &str) {
|
||||
self.string("component", name);
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ListTracer {
|
||||
fn child(&mut self, value: &dyn Trace);
|
||||
fn int(&mut self, i: i64);
|
||||
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);
|
||||
fn bool(&mut self, b: bool);
|
||||
|
||||
fn in_child(&mut self, block: &dyn Fn(&mut dyn Tracer));
|
||||
fn in_list(&mut self, block: &dyn Fn(&mut dyn ListTracer));
|
||||
}
|
||||
|
||||
/// Value that can describe own structure and data using the `Tracer` interface.
|
||||
pub trait Trace {
|
||||
fn trace(&self, d: &mut dyn Tracer);
|
||||
/// Generic tracer based on a TraceWriter.
|
||||
pub struct JsonTracer<F: FnMut(&str)> {
|
||||
write_fn: F,
|
||||
write_buf: [u8; 32],
|
||||
buf_pos: usize,
|
||||
first: bool,
|
||||
}
|
||||
|
||||
impl Trace for &[u8] {
|
||||
fn trace(&self, t: &mut dyn Tracer) {
|
||||
t.bytes(self);
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> Trace for &[u8; N] {
|
||||
fn trace(&self, t: &mut dyn Tracer) {
|
||||
t.bytes(&self[..])
|
||||
}
|
||||
}
|
||||
|
||||
impl Trace for &str {
|
||||
fn trace(&self, t: &mut dyn Tracer) {
|
||||
t.string(self);
|
||||
}
|
||||
}
|
||||
|
||||
impl Trace for usize {
|
||||
fn trace(&self, t: &mut dyn Tracer) {
|
||||
t.int(*self as i64);
|
||||
}
|
||||
}
|
||||
|
||||
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"),
|
||||
impl<F: FnMut(&str)> JsonTracer<F> {
|
||||
pub fn new(write_fn: F) -> Self {
|
||||
Self {
|
||||
write_fn,
|
||||
write_buf: [0; 32],
|
||||
buf_pos: 0,
|
||||
first: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_comma(&mut self) {
|
||||
if !self.first {
|
||||
(self.write_fn)(", ");
|
||||
}
|
||||
self.first = false;
|
||||
}
|
||||
|
||||
fn write_int(&mut self, i: i64) {
|
||||
let s = format_i64(i, &mut self.write_buf).unwrap_or("\"###NUMERR###\"");
|
||||
(self.write_fn)(s);
|
||||
}
|
||||
|
||||
// SAFETY: there must be a valid utf8 string in write_buf[..buf_pos]
|
||||
unsafe fn flush_buf(&mut self) {
|
||||
if self.buf_pos == 0 {
|
||||
return;
|
||||
}
|
||||
let substr = &self.write_buf[..self.buf_pos];
|
||||
let s = unsafe { core::str::from_utf8_unchecked(substr) };
|
||||
(self.write_fn)(s);
|
||||
self.buf_pos = 0;
|
||||
}
|
||||
|
||||
// SAFETY: there must be a valid utf8 string in write_buf[..buf_pos].
|
||||
// Given that there is valid utf8 at start, there will be valid utf8 when done.
|
||||
unsafe fn push_or_flush(&mut self, ch: char) {
|
||||
let size = ch.len_utf8();
|
||||
if self.buf_pos + size > self.write_buf.len() {
|
||||
unsafe { self.flush_buf() };
|
||||
}
|
||||
ch.encode_utf8(&mut self.write_buf[self.buf_pos..]);
|
||||
self.buf_pos += size;
|
||||
}
|
||||
|
||||
fn write_str_quoted(&mut self, s: &str) {
|
||||
self.buf_pos = 0;
|
||||
// SAFETY: we clear the writebuf by resetting buf_pos to 0, so its contents
|
||||
// are only affected by push_or_flush, which keeps contents of write_buf
|
||||
// correct.
|
||||
unsafe {
|
||||
self.push_or_flush('"');
|
||||
for mut ch in s.chars() {
|
||||
if ch == '\\' || ch == '"' {
|
||||
self.push_or_flush('\\');
|
||||
} else if ch == '\n' {
|
||||
self.push_or_flush('\\');
|
||||
ch = 'n';
|
||||
}
|
||||
self.push_or_flush(ch);
|
||||
}
|
||||
self.push_or_flush('"');
|
||||
self.flush_buf();
|
||||
}
|
||||
}
|
||||
|
||||
fn key(&mut self, key: &str) {
|
||||
self.maybe_comma();
|
||||
self.write_str_quoted(key);
|
||||
(self.write_fn)(": ");
|
||||
}
|
||||
|
||||
pub fn root(&mut self, block: &dyn Fn(&mut dyn Tracer)) {
|
||||
(self.write_fn)("{");
|
||||
block(self);
|
||||
(self.write_fn)("}");
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: FnMut(&str)> ListTracer for JsonTracer<F> {
|
||||
fn child(&mut self, value: &dyn Trace) {
|
||||
ListTracer::in_child(self, &mut |t| value.trace(t));
|
||||
}
|
||||
|
||||
fn int(&mut self, i: i64) {
|
||||
self.maybe_comma();
|
||||
self.write_int(i);
|
||||
}
|
||||
|
||||
fn string(&mut self, s: &str) {
|
||||
self.maybe_comma();
|
||||
self.write_str_quoted(s);
|
||||
}
|
||||
|
||||
fn bool(&mut self, b: bool) {
|
||||
self.maybe_comma();
|
||||
(self.write_fn)(if b { "true" } else { "false" });
|
||||
}
|
||||
|
||||
fn in_child(&mut self, block: &dyn Fn(&mut dyn Tracer)) {
|
||||
self.maybe_comma();
|
||||
self.first = true;
|
||||
(self.write_fn)("{");
|
||||
block(self);
|
||||
(self.write_fn)("}");
|
||||
self.first = false;
|
||||
}
|
||||
|
||||
fn in_list(&mut self, block: &dyn Fn(&mut dyn ListTracer)) {
|
||||
self.maybe_comma();
|
||||
self.first = true;
|
||||
(self.write_fn)("[");
|
||||
block(self);
|
||||
(self.write_fn)("]");
|
||||
self.first = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: FnMut(&str)> Tracer for JsonTracer<F> {
|
||||
fn child(&mut self, key: &str, value: &dyn Trace) {
|
||||
Tracer::in_child(self, key, &|t| value.trace(t));
|
||||
}
|
||||
|
||||
fn int(&mut self, key: &str, i: i64) {
|
||||
self.key(key);
|
||||
self.write_int(i);
|
||||
}
|
||||
|
||||
fn string(&mut self, key: &str, s: &str) {
|
||||
self.key(key);
|
||||
self.write_str_quoted(s);
|
||||
}
|
||||
|
||||
fn bool(&mut self, key: &str, b: bool) {
|
||||
self.key(key);
|
||||
(self.write_fn)(if b { "true" } else { "false" });
|
||||
}
|
||||
|
||||
fn in_child(&mut self, key: &str, block: &dyn Fn(&mut dyn Tracer)) {
|
||||
self.key(key);
|
||||
(self.write_fn)("{");
|
||||
self.first = true;
|
||||
block(self);
|
||||
(self.write_fn)("}");
|
||||
self.first = false;
|
||||
}
|
||||
|
||||
fn in_list(&mut self, key: &str, block: &dyn Fn(&mut dyn ListTracer)) {
|
||||
self.key(key);
|
||||
(self.write_fn)("[");
|
||||
self.first = true;
|
||||
block(self);
|
||||
(self.write_fn)("]");
|
||||
self.first = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Value that can describe own structure and data using the `Tracer`
|
||||
/// interface.
|
||||
pub trait Trace {
|
||||
fn trace(&self, t: &mut dyn Tracer);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
pub mod tests {
|
||||
extern crate serde_json;
|
||||
use serde_json::Value;
|
||||
|
||||
use super::*;
|
||||
|
||||
impl Tracer for Vec<u8> {
|
||||
fn int(&mut self, i: i64) {
|
||||
self.string(&i.to_string());
|
||||
}
|
||||
|
||||
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">")
|
||||
}
|
||||
pub fn trace(val: &impl Trace) -> Value {
|
||||
let mut buf = Vec::new();
|
||||
let mut tracer = JsonTracer::new(|text| buf.extend_from_slice(text.as_bytes()));
|
||||
tracer.root(&|t| val.trace(t));
|
||||
let s = String::from_utf8(buf).unwrap();
|
||||
//crate::micropython::print::print(s.as_str());
|
||||
s.parse().unwrap()
|
||||
}
|
||||
}
|
||||
|
@ -211,16 +211,14 @@ where
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T, U> crate::trace::Trace for (T, U)
|
||||
where
|
||||
T: Component,
|
||||
T: crate::trace::Trace,
|
||||
U: Component,
|
||||
U: crate::trace::Trace,
|
||||
{
|
||||
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
|
||||
d.open("Tuple");
|
||||
d.field("0", &self.0);
|
||||
d.field("1", &self.1);
|
||||
d.close();
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.in_list("children", &mut |l| {
|
||||
l.child(&self.0);
|
||||
l.child(&self.1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -260,25 +258,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T, U, V> crate::trace::Trace for (T, U, V)
|
||||
where
|
||||
T: Component,
|
||||
T: crate::trace::Trace,
|
||||
U: Component,
|
||||
U: crate::trace::Trace,
|
||||
V: Component,
|
||||
V: crate::trace::Trace,
|
||||
{
|
||||
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
|
||||
d.open("Tuple");
|
||||
d.field("0", &self.0);
|
||||
d.field("1", &self.1);
|
||||
d.field("2", &self.2);
|
||||
d.close();
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Component for Option<T>
|
||||
where
|
||||
T: Component,
|
||||
|
@ -20,7 +20,6 @@ impl Component for Empty {
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for Empty {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("Empty");
|
||||
t.close();
|
||||
t.component("Empty");
|
||||
}
|
||||
}
|
||||
|
@ -63,8 +63,7 @@ impl Component for Image {
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for Image {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("Image");
|
||||
t.close();
|
||||
t.component("Image");
|
||||
}
|
||||
}
|
||||
|
||||
@ -139,7 +138,6 @@ impl Component for BlendedImage {
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for BlendedImage {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("BlendedImage");
|
||||
t.close();
|
||||
t.component("BlendedImage");
|
||||
}
|
||||
}
|
||||
|
@ -117,6 +117,7 @@ where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.string(self.text.as_ref())
|
||||
t.component("Label");
|
||||
t.string("text", self.text.as_ref());
|
||||
}
|
||||
}
|
||||
|
@ -232,9 +232,8 @@ impl<T> crate::trace::Trace for Marquee<T>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
|
||||
d.open("Marquee");
|
||||
d.field("text", &self.text.as_ref());
|
||||
d.close();
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("Marquee");
|
||||
t.string("text", self.text.as_ref());
|
||||
}
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ where
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<F> crate::trace::Trace for Painter<F> {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.string("Painter")
|
||||
t.component("Painter");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -71,10 +71,9 @@ where
|
||||
T: Component,
|
||||
T: crate::trace::Trace,
|
||||
{
|
||||
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
|
||||
d.open("GridPlaced");
|
||||
d.field("inner", &self.inner);
|
||||
d.close();
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("GridPlaced");
|
||||
t.child("inner", &self.inner);
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,10 +114,9 @@ where
|
||||
T: Component,
|
||||
T: crate::trace::Trace,
|
||||
{
|
||||
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
|
||||
d.open("FixedHeightBar");
|
||||
d.field("inner", &self.inner);
|
||||
d.close();
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("FixedHeightBar");
|
||||
t.child("inner", &self.inner);
|
||||
}
|
||||
}
|
||||
|
||||
@ -188,10 +186,9 @@ where
|
||||
T: Component,
|
||||
T: crate::trace::Trace,
|
||||
{
|
||||
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
|
||||
d.open("Floating");
|
||||
d.field("inner", &self.inner);
|
||||
d.close();
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("Floating");
|
||||
t.child("inner", &self.inner);
|
||||
}
|
||||
}
|
||||
|
||||
@ -277,13 +274,12 @@ where
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T, U> crate::trace::Trace for Split<T, U>
|
||||
where
|
||||
T: Component + crate::trace::Trace,
|
||||
U: Component + crate::trace::Trace,
|
||||
T: crate::trace::Trace,
|
||||
U: crate::trace::Trace,
|
||||
{
|
||||
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
|
||||
d.open("Split");
|
||||
d.field("first", &self.first);
|
||||
d.field("second", &self.second);
|
||||
d.close();
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("Split");
|
||||
t.child("first", &self.first);
|
||||
t.child("second", &self.second);
|
||||
}
|
||||
}
|
||||
|
@ -150,8 +150,7 @@ impl Component for Qr {
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for Qr {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("Qr");
|
||||
t.field("text", &self.text.as_str());
|
||||
t.close();
|
||||
t.component("Qr");
|
||||
t.string("text", self.text.as_ref());
|
||||
}
|
||||
}
|
||||
|
@ -144,25 +144,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
pub mod trace {
|
||||
use crate::ui::component::text::layout::trace::TraceSink;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub struct TraceText<'a, F, T>(pub &'a FormattedText<F, T>);
|
||||
|
||||
impl<'a, F, T> crate::trace::Trace for TraceText<'a, F, T>
|
||||
where
|
||||
F: AsRef<str>,
|
||||
T: AsRef<str>,
|
||||
{
|
||||
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 FormattedText<F, T>
|
||||
where
|
||||
@ -170,9 +151,15 @@ where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("Text");
|
||||
t.field("content", &trace::TraceText(self));
|
||||
t.close();
|
||||
use crate::ui::component::text::layout::trace::TraceSink;
|
||||
use core::cell::Cell;
|
||||
let fit: Cell<Option<LayoutFit>> = Cell::new(None);
|
||||
t.component("FormattedText");
|
||||
t.in_list("text", &mut |l| {
|
||||
let result = self.layout_content(&mut TraceSink(l));
|
||||
fit.set(Some(result));
|
||||
});
|
||||
t.bool("fits", matches!(fit.get(), Some(LayoutFit::Fitting { .. })));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -445,31 +445,32 @@ impl LayoutSink for TextRenderer {
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
pub mod trace {
|
||||
use crate::ui::geometry::Point;
|
||||
use crate::{trace::ListTracer, ui::geometry::Point};
|
||||
|
||||
use super::*;
|
||||
|
||||
pub struct TraceSink<'a>(pub &'a mut dyn crate::trace::Tracer);
|
||||
/// `LayoutSink` for debugging purposes.
|
||||
pub struct TraceSink<'a>(pub &'a mut dyn ListTracer);
|
||||
|
||||
impl<'a> LayoutSink for TraceSink<'a> {
|
||||
impl LayoutSink for TraceSink<'_> {
|
||||
fn text(&mut self, _cursor: Point, _layout: &TextLayout, text: &str) {
|
||||
self.0.string(text);
|
||||
self.0.string(&text);
|
||||
}
|
||||
|
||||
fn hyphen(&mut self, _cursor: Point, _layout: &TextLayout) {
|
||||
self.0.string("-");
|
||||
self.0.string(&"-");
|
||||
}
|
||||
|
||||
fn ellipsis(&mut self, _cursor: Point, _layout: &TextLayout) {
|
||||
self.0.string("...");
|
||||
self.0.string(&ELLIPSIS);
|
||||
}
|
||||
|
||||
fn prev_page_ellipsis(&mut self, _cursor: Point, _layout: &TextLayout) {
|
||||
self.0.string("...");
|
||||
self.0.string(&ELLIPSIS);
|
||||
}
|
||||
|
||||
fn line_break(&mut self, _cursor: Point) {
|
||||
self.0.string("\n");
|
||||
self.0.string(&"\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -238,17 +238,23 @@ pub mod trace {
|
||||
|
||||
impl<T: ParagraphSource> crate::trace::Trace for Paragraphs<T> {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("Paragraphs");
|
||||
Self::foreach_visible(
|
||||
&self.source,
|
||||
&self.visible,
|
||||
self.offset,
|
||||
&mut |layout, content| {
|
||||
layout.layout_text(content, &mut layout.initial_cursor(), &mut TraceSink(t));
|
||||
t.string("\n");
|
||||
},
|
||||
);
|
||||
t.close();
|
||||
t.string("component", "Paragraphs");
|
||||
t.in_list("paragraphs", &mut |par_list| {
|
||||
Self::foreach_visible(
|
||||
&self.source,
|
||||
&self.visible,
|
||||
self.offset,
|
||||
&mut |layout, content| {
|
||||
par_list.in_list(&mut |par| {
|
||||
layout.layout_text(
|
||||
content,
|
||||
&mut layout.initial_cursor(),
|
||||
&mut TraceSink(par),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -606,10 +612,9 @@ where
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T: ParagraphSource> crate::trace::Trace for Checklist<T> {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("Checklist");
|
||||
t.field("current", &self.current);
|
||||
t.field("items", &self.paragraphs);
|
||||
t.close();
|
||||
t.component("Checklist");
|
||||
t.int("current", self.current as i64);
|
||||
t.child("items", &self.paragraphs);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -53,8 +53,7 @@ impl Component for Timeout {
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for Timeout {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("Timeout");
|
||||
t.int(self.time_ms as i64);
|
||||
t.close();
|
||||
t.component("Timeout");
|
||||
t.int("time_ms", self.time_ms as i64);
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ use core::{
|
||||
|
||||
use crate::{
|
||||
error::Error,
|
||||
maybe_trace::MaybeTrace,
|
||||
micropython::{
|
||||
buffer::StrBuffer,
|
||||
gc::Gc,
|
||||
@ -35,24 +36,6 @@ pub trait ComponentMsgObj: Component {
|
||||
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error>;
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
mod maybe_trace {
|
||||
pub trait MaybeTrace: crate::trace::Trace {}
|
||||
impl<T> MaybeTrace for T where T: crate::trace::Trace {}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ui_debug"))]
|
||||
mod maybe_trace {
|
||||
pub trait MaybeTrace {}
|
||||
impl<T> MaybeTrace for T {}
|
||||
}
|
||||
|
||||
/// Stand-in for the optionally-compiled trait `Trace`.
|
||||
/// If UI debugging is enabled, `MaybeTrace` implies `Trace` and is implemented
|
||||
/// for everything that implements Trace. If disabled, `MaybeTrace` is
|
||||
/// implemented for everything.
|
||||
use maybe_trace::MaybeTrace;
|
||||
|
||||
/// Object-safe interface between trait `Component` and MicroPython world. It
|
||||
/// converts the result of `Component::event` into `Obj` via the
|
||||
/// `ComponentMsgObj` trait, in order to easily return the value to Python. It
|
||||
@ -213,55 +196,23 @@ impl LayoutObj {
|
||||
/// raises an exception.
|
||||
#[cfg(feature = "ui_debug")]
|
||||
fn obj_trace(&self, callback: Obj) {
|
||||
use crate::trace::{Trace, Tracer};
|
||||
use crate::trace::JsonTracer;
|
||||
|
||||
struct CallbackTracer(Obj);
|
||||
let mut tracer = JsonTracer::new(|text: &str| {
|
||||
unwrap!(callback.call_with_n_args(&[unwrap!(text.try_into())]));
|
||||
});
|
||||
|
||||
impl Tracer for CallbackTracer {
|
||||
fn int(&mut self, i: i64) {
|
||||
self.0.call_with_n_args(&[i.try_into().unwrap()]).unwrap();
|
||||
}
|
||||
// For Reasons(tm), we must pass a closure in which we call `root.trace(t)`,
|
||||
// instead of passing `root` into the tracer.
|
||||
|
||||
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(&[
|
||||
"<".try_into().unwrap(),
|
||||
name.try_into().unwrap(),
|
||||
">".try_into().unwrap(),
|
||||
])
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn open(&mut self, name: &str) {
|
||||
self.0
|
||||
.call_with_n_args(&["<".try_into().unwrap(), name.try_into().unwrap()])
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn field(&mut self, name: &str, value: &dyn Trace) {
|
||||
self.0
|
||||
.call_with_n_args(&[name.try_into().unwrap(), ": ".try_into().unwrap()])
|
||||
.unwrap();
|
||||
value.trace(self);
|
||||
}
|
||||
|
||||
fn close(&mut self) {
|
||||
self.0.call_with_n_args(&[">".try_into().unwrap()]).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
self.inner
|
||||
.borrow()
|
||||
.root
|
||||
.trace(&mut CallbackTracer(callback));
|
||||
// (The Reasons being, root is a `Gc<dyn ObjComponent>`, and `Gc` does not
|
||||
// implement `Trace`, and `dyn ObjComponent` is unsized so we can't deref it to
|
||||
// claim that it implements `Trace`, and we also can't upcast it to `&dyn Trace`
|
||||
// because trait upcasting is unstable.
|
||||
// Luckily, calling `root.trace()` works perfectly fine in spite of the above.)
|
||||
tracer.root(&|t| {
|
||||
self.inner.borrow().root.trace(t);
|
||||
});
|
||||
}
|
||||
|
||||
fn obj_page_count(&self) -> Obj {
|
||||
|
@ -155,15 +155,14 @@ where
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T> crate::trace::Trace for Button<T>
|
||||
where
|
||||
T: AsRef<str> + crate::trace::Trace,
|
||||
T: AsRef<str>,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("Button");
|
||||
t.component("Button");
|
||||
match &self.content {
|
||||
ButtonContent::Text(text) => t.field("text", text),
|
||||
ButtonContent::Icon(_) => t.symbol("icon"),
|
||||
ButtonContent::Text(text) => t.string("text", text.as_ref()),
|
||||
ButtonContent::Icon(_) => t.bool("icon", true),
|
||||
}
|
||||
t.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -82,9 +82,8 @@ impl Component for HoldToConfirm {
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for HoldToConfirm {
|
||||
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
|
||||
d.open("HoldToConfirm");
|
||||
self.loader.trace(d);
|
||||
d.close();
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("HoldToConfirm");
|
||||
t.child("loader", &self.loader);
|
||||
}
|
||||
}
|
||||
|
@ -81,14 +81,9 @@ where
|
||||
U: crate::trace::Trace + AsRef<str>,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("Dialog");
|
||||
t.field("content", &self.content);
|
||||
if let Some(label) = &self.left_btn {
|
||||
t.field("left", label);
|
||||
}
|
||||
if let Some(label) = &self.right_btn {
|
||||
t.field("right", label);
|
||||
}
|
||||
t.close();
|
||||
t.component("Dialog");
|
||||
t.child("content", &self.content);
|
||||
self.left_btn.as_ref().map(|b| t.child("left", b));
|
||||
self.right_btn.as_ref().map(|b| t.child("right", b));
|
||||
}
|
||||
}
|
||||
|
@ -68,12 +68,11 @@ where
|
||||
impl<T, U> crate::trace::Trace for Frame<T, U>
|
||||
where
|
||||
T: crate::trace::Trace,
|
||||
U: crate::trace::Trace + AsRef<str>,
|
||||
U: AsRef<str>,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("Frame");
|
||||
t.field("title", &self.title);
|
||||
t.field("content", &self.content);
|
||||
t.close();
|
||||
t.component("Frame");
|
||||
t.string("title", self.title.as_ref());
|
||||
t.child("content", &self.content);
|
||||
}
|
||||
}
|
||||
|
@ -193,8 +193,7 @@ pub struct LoaderStyle {
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for Loader {
|
||||
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
|
||||
d.open("Loader");
|
||||
d.close();
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("Loader");
|
||||
}
|
||||
}
|
||||
|
@ -119,11 +119,10 @@ where
|
||||
T: crate::trace::Trace,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("ButtonPage");
|
||||
t.field("active_page", &self.scrollbar.active_page);
|
||||
t.field("page_count", &self.scrollbar.page_count);
|
||||
t.field("content", &self.content);
|
||||
t.close();
|
||||
t.component("ButtonPage");
|
||||
t.int("active_page", self.scrollbar.active_page as i64);
|
||||
t.int("page_count", self.scrollbar.page_count as i64);
|
||||
t.child("content", &self.content);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -143,8 +143,7 @@ impl Component for ResultAnim {
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for ResultAnim {
|
||||
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
|
||||
d.open("ResultAnim");
|
||||
d.close();
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("ResultAnim");
|
||||
}
|
||||
}
|
||||
|
@ -157,12 +157,11 @@ impl<S: ParagraphStrType> Component for ResultPopup<S> {
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<S: ParagraphStrType> crate::trace::Trace for ResultPopup<S> {
|
||||
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
|
||||
d.open("ResultPopup");
|
||||
self.text.trace(d);
|
||||
self.button.trace(d);
|
||||
self.headline.trace(d);
|
||||
self.result_anim.trace(d);
|
||||
d.close();
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("ResultPopup");
|
||||
t.child("text", &self.text);
|
||||
self.button.as_ref().map(|b| t.child("button", b));
|
||||
self.headline.as_ref().map(|h| t.child("headline", h));
|
||||
t.child("result_anim", &self.result_anim);
|
||||
}
|
||||
}
|
||||
|
@ -147,8 +147,10 @@ pub static mp_module_trezorui2: Module = obj_module! {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
extern crate json;
|
||||
|
||||
use crate::{
|
||||
trace::Trace,
|
||||
trace::tests::trace,
|
||||
ui::{
|
||||
component::Component,
|
||||
model_tr::{
|
||||
@ -160,12 +162,6 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
fn trace(val: &impl Trace) -> String {
|
||||
let mut t = Vec::new();
|
||||
val.trace(&mut t);
|
||||
String::from_utf8(t).unwrap()
|
||||
}
|
||||
|
||||
impl<T, U> ComponentMsgObj for Dialog<T, U>
|
||||
where
|
||||
T: ComponentMsgObj,
|
||||
|
@ -194,15 +194,14 @@ where
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T> crate::trace::Trace for AddressDetails<T>
|
||||
where
|
||||
T: ParagraphStrType + crate::trace::Trace,
|
||||
T: ParagraphStrType,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("AddressDetails");
|
||||
t.component("AddressDetails");
|
||||
match self.current_page {
|
||||
0 => self.qr_code.trace(t),
|
||||
1 => self.details.trace(t),
|
||||
_ => self.xpub_view.trace(t),
|
||||
0 => t.child("qr_code", &self.qr_code),
|
||||
1 => t.child("details", &self.details),
|
||||
_ => t.child("xpub_view", &self.xpub_view),
|
||||
}
|
||||
t.close();
|
||||
}
|
||||
}
|
||||
|
@ -323,18 +323,20 @@ where
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T> crate::trace::Trace for Button<T>
|
||||
where
|
||||
T: AsRef<str> + crate::trace::Trace,
|
||||
T: AsRef<str>,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("Button");
|
||||
t.component("Button");
|
||||
match &self.content {
|
||||
ButtonContent::Empty => {}
|
||||
ButtonContent::Text(text) => t.field("text", text),
|
||||
ButtonContent::Icon(_) => t.symbol("icon"),
|
||||
ButtonContent::IconAndText(_) => {}
|
||||
ButtonContent::IconBlend(_, _, _) => t.symbol("icon"),
|
||||
ButtonContent::Text(text) => t.string("text", text.as_ref()),
|
||||
ButtonContent::Icon(_) => t.bool("icon", true),
|
||||
ButtonContent::IconAndText(content) => {
|
||||
t.string("text", content.text);
|
||||
t.bool("icon", true);
|
||||
}
|
||||
ButtonContent::IconBlend(_, _, _) => t.bool("icon", true),
|
||||
}
|
||||
t.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,16 @@
|
||||
use core::mem;
|
||||
|
||||
use crate::ui::{
|
||||
component::{
|
||||
base::Never, painter, Child, Component, ComponentExt, Empty, Event, EventCtx, Label, Split,
|
||||
use crate::{
|
||||
maybe_trace::MaybeTrace,
|
||||
ui::{
|
||||
component::{
|
||||
base::Never, painter, Child, Component, ComponentExt, Empty, Event, EventCtx, Label,
|
||||
Split,
|
||||
},
|
||||
display::loader::{loader_circular_uncompress, LoaderDimensions},
|
||||
geometry::{Alignment, Insets, Rect},
|
||||
util::animation_disabled,
|
||||
},
|
||||
display::loader::{loader_circular_uncompress, LoaderDimensions},
|
||||
geometry::{Alignment, Insets, Rect},
|
||||
util::animation_disabled,
|
||||
};
|
||||
|
||||
use super::{theme, Frame};
|
||||
@ -31,7 +35,10 @@ impl<T, U> CoinJoinProgress<T, U>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
pub fn new(text: T, indeterminate: bool) -> CoinJoinProgress<T, impl Component<Msg = Never>>
|
||||
pub fn new(
|
||||
text: T,
|
||||
indeterminate: bool,
|
||||
) -> CoinJoinProgress<T, impl Component<Msg = Never> + MaybeTrace>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
@ -121,11 +128,12 @@ where
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T, U> crate::trace::Trace for CoinJoinProgress<T, U>
|
||||
where
|
||||
T: crate::trace::Trace,
|
||||
U: Component,
|
||||
T: AsRef<str>,
|
||||
U: Component + crate::trace::Trace,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("CoinJoinProgress");
|
||||
t.close();
|
||||
t.component("CoinJoinProgress");
|
||||
t.child("label", &self.label);
|
||||
t.child("content", &self.content);
|
||||
}
|
||||
}
|
||||
|
@ -85,10 +85,9 @@ where
|
||||
U: crate::trace::Trace,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("Dialog");
|
||||
t.field("content", &self.content);
|
||||
t.field("controls", &self.controls);
|
||||
t.close();
|
||||
t.component("Dialog");
|
||||
t.child("content", &self.content);
|
||||
t.child("controls", &self.controls);
|
||||
}
|
||||
}
|
||||
|
||||
@ -208,10 +207,9 @@ where
|
||||
U: crate::trace::Trace,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("IconDialog");
|
||||
t.field("content", &self.paragraphs);
|
||||
t.field("image", &self.image);
|
||||
t.field("controls", &self.controls);
|
||||
t.close();
|
||||
t.component("IconDialog");
|
||||
t.child("image", &self.image);
|
||||
t.child("content", &self.paragraphs);
|
||||
t.child("controls", &self.controls);
|
||||
}
|
||||
}
|
||||
|
@ -205,7 +205,6 @@ where
|
||||
F: Fn(usize) -> T,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("FidoPaginatedPage");
|
||||
t.close();
|
||||
t.component("FidoConfirm");
|
||||
}
|
||||
}
|
||||
|
@ -186,19 +186,13 @@ where
|
||||
impl<T, U> crate::trace::Trace for Frame<T, U>
|
||||
where
|
||||
T: crate::trace::Trace,
|
||||
U: crate::trace::Trace + AsRef<str>,
|
||||
U: AsRef<str>,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("Frame");
|
||||
t.field("title", &self.title);
|
||||
if let Some(s) = &self.subtitle {
|
||||
t.field("subtitle", s);
|
||||
}
|
||||
if let Some(b) = &self.button {
|
||||
t.field("button", b);
|
||||
}
|
||||
t.field("content", &self.content);
|
||||
t.close();
|
||||
t.component("Frame");
|
||||
t.child("title", &self.title);
|
||||
self.subtitle.as_ref().map(|s| t.child("subtitle", s));
|
||||
self.button.as_ref().map(|b| t.child("button", b));
|
||||
}
|
||||
}
|
||||
|
||||
@ -279,12 +273,11 @@ where
|
||||
impl<T, U> crate::trace::Trace for NotificationFrame<T, U>
|
||||
where
|
||||
T: crate::trace::Trace,
|
||||
U: crate::trace::Trace + AsRef<str>,
|
||||
U: AsRef<str>,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("NotificationFrame");
|
||||
t.field("title", &self.title);
|
||||
t.field("content", &self.content);
|
||||
t.close();
|
||||
t.component("NotificationFrame");
|
||||
t.string("title", self.title.as_ref());
|
||||
t.child("content", &self.content);
|
||||
}
|
||||
}
|
||||
|
@ -108,10 +108,9 @@ impl<T> crate::trace::Trace for HoldToConfirm<T>
|
||||
where
|
||||
T: crate::trace::Trace,
|
||||
{
|
||||
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
|
||||
d.open("HoldToConfirm");
|
||||
self.content.trace(d);
|
||||
d.close();
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("HoldToConfirm");
|
||||
t.child("content", &self.content);
|
||||
}
|
||||
}
|
||||
|
||||
@ -181,8 +180,8 @@ impl Component for CancelHold {
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for CancelHold {
|
||||
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
|
||||
d.string("CancelHold")
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("CancelHold");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -258,10 +258,9 @@ where
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T: AsRef<str>> crate::trace::Trace for Homescreen<T> {
|
||||
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
|
||||
d.open("Homescreen");
|
||||
d.field("label", &self.label.as_ref());
|
||||
d.close();
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("Homescreen");
|
||||
t.string("label", self.label.as_ref());
|
||||
}
|
||||
}
|
||||
|
||||
@ -392,8 +391,7 @@ fn is_image_toif(buffer: &[u8]) -> bool {
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T> crate::trace::Trace for Lockscreen<T> {
|
||||
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
|
||||
d.open("Lockscreen");
|
||||
d.close();
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("Lockscreen");
|
||||
}
|
||||
}
|
||||
|
@ -138,10 +138,9 @@ where
|
||||
T: crate::trace::Trace,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("HorizontalPage");
|
||||
t.field("active_page", &self.scrollbar.active_page);
|
||||
t.field("page_count", &self.scrollbar.page_count);
|
||||
t.field("content", &self.content);
|
||||
t.close();
|
||||
t.component("HorizontalPage");
|
||||
t.int("active_page", self.scrollbar.active_page as i64);
|
||||
t.int("page_count", self.scrollbar.page_count as i64);
|
||||
t.child("content", &self.content);
|
||||
}
|
||||
}
|
||||
|
@ -193,7 +193,6 @@ pub enum MnemonicInputMsg {
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T, U> crate::trace::Trace for MnemonicKeyboard<T, U> {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("MnemonicKeyboard");
|
||||
t.close();
|
||||
t.component("MnemonicKeyboard");
|
||||
}
|
||||
}
|
||||
|
@ -377,7 +377,6 @@ impl Component for Input {
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for PassphraseKeyboard {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("PassphraseKeyboard");
|
||||
t.close();
|
||||
t.component("PassphraseKeyboard");
|
||||
}
|
||||
}
|
||||
|
@ -467,7 +467,6 @@ where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("PinKeyboard");
|
||||
t.close();
|
||||
t.component("PinKeyboard");
|
||||
}
|
||||
}
|
||||
|
@ -68,7 +68,6 @@ impl Component for SelectWordCount {
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for SelectWordCount {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("SelectWordCount");
|
||||
t.close();
|
||||
t.component("SelectWordCount");
|
||||
}
|
||||
}
|
||||
|
@ -198,9 +198,8 @@ pub struct LoaderStyle {
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for Loader {
|
||||
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
|
||||
d.open("Loader");
|
||||
d.close();
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("Loader");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -135,12 +135,11 @@ where
|
||||
F: Fn(u32) -> T,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("NumberInputDialog");
|
||||
t.field("input", &self.input);
|
||||
t.field("paragraphs", &self.paragraphs);
|
||||
t.field("info_button", &self.info_button);
|
||||
t.field("confirm_button", &self.confirm_button);
|
||||
t.close();
|
||||
t.component("NumberInputDialog");
|
||||
t.child("input", &self.input);
|
||||
t.child("paragraphs", &self.paragraphs);
|
||||
t.child("info_button", &self.info_button);
|
||||
t.child("confirm_button", &self.confirm_button);
|
||||
}
|
||||
}
|
||||
|
||||
@ -238,8 +237,7 @@ impl Component for NumberInput {
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for NumberInput {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("NumberInput");
|
||||
t.field("value", &(self.value as usize));
|
||||
t.close();
|
||||
t.component("NumberInput");
|
||||
t.int("value", self.value as i64);
|
||||
}
|
||||
}
|
||||
|
@ -311,12 +311,11 @@ where
|
||||
U: crate::trace::Trace + Component,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("SwipePage");
|
||||
t.field("active_page", &self.scrollbar.active_page);
|
||||
t.field("page_count", &self.scrollbar.page_count);
|
||||
t.field("content", &self.content);
|
||||
t.field("controls", &self.controls);
|
||||
t.close();
|
||||
t.component("SwipePage");
|
||||
t.int("active_page", self.scrollbar.active_page as i64);
|
||||
t.int("page_count", self.scrollbar.page_count as i64);
|
||||
t.child("content", &self.content);
|
||||
t.child("controls", &self.controls);
|
||||
}
|
||||
}
|
||||
|
||||
@ -495,8 +494,10 @@ where
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
extern crate serde_json;
|
||||
|
||||
use crate::{
|
||||
trace::Trace,
|
||||
trace::tests::trace,
|
||||
ui::{
|
||||
component::{
|
||||
text::paragraphs::{Paragraph, ParagraphStrType, Paragraphs},
|
||||
@ -518,12 +519,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn trace(val: &impl Trace) -> String {
|
||||
let mut t = Vec::new();
|
||||
val.trace(&mut t);
|
||||
String::from_utf8(t).unwrap()
|
||||
}
|
||||
|
||||
fn swipe(component: &mut impl Component, points: &[(i16, i16)]) {
|
||||
let last = points.len().saturating_sub(1);
|
||||
let mut first = true;
|
||||
@ -560,8 +555,18 @@ mod tests {
|
||||
);
|
||||
page.place(SCREEN);
|
||||
|
||||
let expected =
|
||||
"<SwipePage active_page:0 page_count:1 content:<Paragraphs > controls:<Empty > >";
|
||||
let expected = serde_json::json!({
|
||||
"component": "SwipePage",
|
||||
"active_page": 0,
|
||||
"page_count": 1,
|
||||
"content": {
|
||||
"component": "Paragraphs",
|
||||
"paragraphs": [],
|
||||
},
|
||||
"controls": {
|
||||
"component": "Empty",
|
||||
},
|
||||
});
|
||||
|
||||
assert_eq!(trace(&page), expected);
|
||||
swipe_up(&mut page);
|
||||
@ -588,7 +593,21 @@ mod tests {
|
||||
);
|
||||
page.place(SCREEN);
|
||||
|
||||
let expected = "<SwipePage active_page:0 page_count:1 content:<Paragraphs This is the first\nparagraph and it should\nfit on the screen\nentirely.\nSecond, bold, paragraph\nshould also fit on the\nscreen whole I think.\n> controls:<Empty > >";
|
||||
let expected = serde_json::json!({
|
||||
"component": "SwipePage",
|
||||
"active_page": 0,
|
||||
"page_count": 1,
|
||||
"content": {
|
||||
"component": "Paragraphs",
|
||||
"paragraphs": [
|
||||
["This is the first", "\n", "paragraph and it should", "\n", "fit on the screen", "\n", "entirely."],
|
||||
["Second, bold, paragraph", "\n", "should also fit on the", "\n", "screen whole I think."],
|
||||
],
|
||||
},
|
||||
"controls": {
|
||||
"component": "Empty",
|
||||
},
|
||||
});
|
||||
|
||||
assert_eq!(trace(&page), expected);
|
||||
swipe_up(&mut page);
|
||||
@ -611,18 +630,61 @@ mod tests {
|
||||
);
|
||||
page.place(SCREEN);
|
||||
|
||||
let expected1 = "<SwipePage active_page:0 page_count:2 content:<Paragraphs This is somewhat long\nparagraph that goes on\nand on and on and on and\non and will definitely not\nfit on just a single\nscreen. You have to\nswipe a bit to see all the\ntext it contains I guess....\n> controls:<FixedHeightBar inner:<Button text:NO > > >";
|
||||
let expected2 = "<SwipePage active_page:1 page_count:2 content:<Paragraphs There's just so much\nletters in it.\n> controls:<FixedHeightBar inner:<Button text:NO > > >";
|
||||
let first_page = serde_json::json!({
|
||||
"component": "SwipePage",
|
||||
"active_page": 0,
|
||||
"page_count": 2,
|
||||
"content": {
|
||||
"component": "Paragraphs",
|
||||
"paragraphs": [
|
||||
[
|
||||
"This is somewhat long", "\n",
|
||||
"paragraph that goes on", "\n",
|
||||
"and on and on and on and", "\n",
|
||||
"on and will definitely not", "\n",
|
||||
"fit on just a single", "\n",
|
||||
"screen. You have to", "\n",
|
||||
"swipe a bit to see all the", "\n",
|
||||
"text it contains I guess.", "...",
|
||||
],
|
||||
],
|
||||
},
|
||||
"controls": {
|
||||
"component": "FixedHeightBar",
|
||||
"inner": {
|
||||
"component": "Button",
|
||||
"text": "NO",
|
||||
},
|
||||
},
|
||||
});
|
||||
let second_page = serde_json::json!({
|
||||
"component": "SwipePage",
|
||||
"active_page": 1,
|
||||
"page_count": 2,
|
||||
"content": {
|
||||
"component": "Paragraphs",
|
||||
"paragraphs": [
|
||||
["There's just so much", "\n", "letters in it."],
|
||||
],
|
||||
},
|
||||
"controls": {
|
||||
"component": "FixedHeightBar",
|
||||
"inner": {
|
||||
"component": "Button",
|
||||
"text": "NO",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert_eq!(trace(&page), expected1);
|
||||
assert_eq!(trace(&page), first_page);
|
||||
swipe_down(&mut page);
|
||||
assert_eq!(trace(&page), expected1);
|
||||
assert_eq!(trace(&page), first_page);
|
||||
swipe_up(&mut page);
|
||||
assert_eq!(trace(&page), expected2);
|
||||
assert_eq!(trace(&page), second_page);
|
||||
swipe_up(&mut page);
|
||||
assert_eq!(trace(&page), expected2);
|
||||
assert_eq!(trace(&page), second_page);
|
||||
swipe_down(&mut page);
|
||||
assert_eq!(trace(&page), expected1);
|
||||
assert_eq!(trace(&page), first_page);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -647,25 +709,101 @@ mod tests {
|
||||
);
|
||||
page.place(SCREEN);
|
||||
|
||||
let expected1 = "<SwipePage active_page:0 page_count:3 content:<Paragraphs This paragraph is using a\nbold font. It doesn't need\nto be all that long.\nAnd this one is u\nsing MONO. Monosp\nace is nice for n\numbers, they...\n> controls:<FixedHeightBar inner:<Button text:IDK > > >";
|
||||
let expected2 = "<SwipePage active_page:1 page_count:3 content:<Paragraphs ...have the same\nwidth and can be\nscanned quickly.\nEven if they span\nseveral pages or\nsomething.\nLet's add another one...\n> controls:<FixedHeightBar inner:<Button text:IDK > > >";
|
||||
let expected3 = "<SwipePage active_page:2 page_count:3 content:<Paragraphs for a good measure. This\none should overflow all\nthe way to the third page\nwith a bit of luck.\n> controls:<FixedHeightBar inner:<Button text:IDK > > >";
|
||||
let first_page = serde_json::json!({
|
||||
"component": "SwipePage",
|
||||
"active_page": 0,
|
||||
"page_count": 3,
|
||||
"content": {
|
||||
"component": "Paragraphs",
|
||||
"paragraphs": [
|
||||
[
|
||||
"This paragraph is using a", "\n",
|
||||
"bold font. It doesn't need", "\n",
|
||||
"to be all that long.",
|
||||
],
|
||||
[
|
||||
"And this one is u", "\n",
|
||||
"sing MONO. Monosp", "\n",
|
||||
"ace is nice for n", "\n",
|
||||
"umbers, they", "...",
|
||||
],
|
||||
],
|
||||
},
|
||||
"controls": {
|
||||
"component": "FixedHeightBar",
|
||||
"inner": {
|
||||
"component": "Button",
|
||||
"text": "IDK",
|
||||
},
|
||||
},
|
||||
});
|
||||
let second_page = serde_json::json!({
|
||||
"component": "SwipePage",
|
||||
"active_page": 1,
|
||||
"page_count": 3,
|
||||
"content": {
|
||||
"component": "Paragraphs",
|
||||
"paragraphs": [
|
||||
[
|
||||
"...", "have the same", "\n",
|
||||
"width and can be", "\n",
|
||||
"scanned quickly.", "\n",
|
||||
"Even if they span", "\n",
|
||||
"several pages or", "\n",
|
||||
"something.",
|
||||
],
|
||||
[
|
||||
"Let's add another one", "...",
|
||||
],
|
||||
],
|
||||
},
|
||||
"controls": {
|
||||
"component": "FixedHeightBar",
|
||||
"inner": {
|
||||
"component": "Button",
|
||||
"text": "IDK",
|
||||
},
|
||||
},
|
||||
});
|
||||
let third_page = serde_json::json!({
|
||||
"component": "SwipePage",
|
||||
"active_page": 2,
|
||||
"page_count": 3,
|
||||
"content": {
|
||||
"component": "Paragraphs",
|
||||
"paragraphs": [
|
||||
[
|
||||
"for a good measure. This", "\n",
|
||||
"one should overflow all", "\n",
|
||||
"the way to the third page", "\n",
|
||||
"with a bit of luck.",
|
||||
],
|
||||
],
|
||||
},
|
||||
"controls": {
|
||||
"component": "FixedHeightBar",
|
||||
"inner": {
|
||||
"component": "Button",
|
||||
"text": "IDK",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert_eq!(trace(&page), expected1);
|
||||
assert_eq!(trace(&page), first_page);
|
||||
swipe_down(&mut page);
|
||||
assert_eq!(trace(&page), expected1);
|
||||
assert_eq!(trace(&page), first_page);
|
||||
swipe_up(&mut page);
|
||||
assert_eq!(trace(&page), expected2);
|
||||
assert_eq!(trace(&page), second_page);
|
||||
swipe_up(&mut page);
|
||||
assert_eq!(trace(&page), expected3);
|
||||
assert_eq!(trace(&page), third_page);
|
||||
swipe_up(&mut page);
|
||||
assert_eq!(trace(&page), expected3);
|
||||
assert_eq!(trace(&page), third_page);
|
||||
swipe_down(&mut page);
|
||||
assert_eq!(trace(&page), expected2);
|
||||
assert_eq!(trace(&page), second_page);
|
||||
swipe_down(&mut page);
|
||||
assert_eq!(trace(&page), expected1);
|
||||
assert_eq!(trace(&page), first_page);
|
||||
swipe_down(&mut page);
|
||||
assert_eq!(trace(&page), expected1);
|
||||
assert_eq!(trace(&page), first_page);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -681,16 +819,70 @@ mod tests {
|
||||
);
|
||||
page.place(SCREEN);
|
||||
|
||||
let expected1 = "<SwipePage active_page:0 page_count:3 content:<Paragraphs Short one.\n> controls:<FixedHeightBar inner:<Empty > > >";
|
||||
let expected2 = "<SwipePage active_page:1 page_count:3 content:<Paragraphs Short two.\n> controls:<FixedHeightBar inner:<Empty > > >";
|
||||
let expected3 = "<SwipePage active_page:2 page_count:3 content:<Paragraphs Short three.\n> controls:<FixedHeightBar inner:<Empty > > >";
|
||||
let first_page = serde_json::json!({
|
||||
"component": "SwipePage",
|
||||
"active_page": 0,
|
||||
"page_count": 3,
|
||||
"content": {
|
||||
"component": "Paragraphs",
|
||||
"paragraphs": [
|
||||
[
|
||||
"Short one.",
|
||||
],
|
||||
],
|
||||
},
|
||||
"controls": {
|
||||
"component": "FixedHeightBar",
|
||||
"inner": {
|
||||
"component": "Empty",
|
||||
},
|
||||
},
|
||||
});
|
||||
let second_page = serde_json::json!({
|
||||
"component": "SwipePage",
|
||||
"active_page": 1,
|
||||
"page_count": 3,
|
||||
"content": {
|
||||
"component": "Paragraphs",
|
||||
"paragraphs": [
|
||||
[
|
||||
"Short two.",
|
||||
],
|
||||
],
|
||||
},
|
||||
"controls": {
|
||||
"component": "FixedHeightBar",
|
||||
"inner": {
|
||||
"component": "Empty",
|
||||
},
|
||||
},
|
||||
});
|
||||
let third_page = serde_json::json!({
|
||||
"component": "SwipePage",
|
||||
"active_page": 2,
|
||||
"page_count": 3,
|
||||
"content": {
|
||||
"component": "Paragraphs",
|
||||
"paragraphs": [
|
||||
[
|
||||
"Short three.",
|
||||
],
|
||||
],
|
||||
},
|
||||
"controls": {
|
||||
"component": "FixedHeightBar",
|
||||
"inner": {
|
||||
"component": "Empty",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert_eq!(trace(&page), expected1);
|
||||
assert_eq!(trace(&page), first_page);
|
||||
swipe_up(&mut page);
|
||||
assert_eq!(trace(&page), expected2);
|
||||
assert_eq!(trace(&page), second_page);
|
||||
swipe_up(&mut page);
|
||||
assert_eq!(trace(&page), expected3);
|
||||
assert_eq!(trace(&page), third_page);
|
||||
swipe_up(&mut page);
|
||||
assert_eq!(trace(&page), expected3);
|
||||
assert_eq!(trace(&page), third_page);
|
||||
}
|
||||
}
|
||||
|
@ -133,7 +133,6 @@ where
|
||||
T: ParagraphStrType,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("Progress");
|
||||
t.close();
|
||||
t.component("Progress");
|
||||
}
|
||||
}
|
||||
|
@ -65,8 +65,7 @@ impl Component for WelcomeScreen {
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for WelcomeScreen {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("WelcomeScreen");
|
||||
t.string(MODEL_NAME);
|
||||
t.close();
|
||||
t.component("WelcomeScreen");
|
||||
t.string("model", MODEL_NAME);
|
||||
}
|
||||
}
|
||||
|
@ -1961,25 +1961,17 @@ pub static mp_module_trezorui2: Module = obj_module! {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
extern crate serde_json;
|
||||
|
||||
use crate::{
|
||||
trace::Trace,
|
||||
ui::{
|
||||
component::{Component, FormattedText},
|
||||
geometry::Rect,
|
||||
model_tt::constant,
|
||||
},
|
||||
trace::tests::trace,
|
||||
ui::{geometry::Rect, model_tt::constant},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
const SCREEN: Rect = constant::screen().inset(theme::borders());
|
||||
|
||||
fn trace(val: &impl Trace) -> String {
|
||||
let mut t = std::vec::Vec::new();
|
||||
val.trace(&mut t);
|
||||
String::from_utf8(t).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trace_example_layout() {
|
||||
let buttons =
|
||||
@ -1994,9 +1986,31 @@ mod tests {
|
||||
buttons,
|
||||
);
|
||||
layout.place(SCREEN);
|
||||
assert_eq!(
|
||||
trace(&layout),
|
||||
"<Dialog content:<Text content:Testing text layout, with\nsome text, and some\nmore text. And parame-\nters! > controls:<FixedHeightBar inner:<Split first:<Button text:Left > second:<Button text:Right > > > >",
|
||||
)
|
||||
|
||||
let expected = serde_json::json!({
|
||||
"component": "Dialog",
|
||||
"content": {
|
||||
"component": "FormattedText",
|
||||
"text": ["Testing text layout, with", "\n", "some text, and some", "\n",
|
||||
"more text. And ", "parame", "-", "\n", "ters!"],
|
||||
"fits": true,
|
||||
},
|
||||
"controls": {
|
||||
"component": "FixedHeightBar",
|
||||
"inner": {
|
||||
"component": "Split",
|
||||
"first": {
|
||||
"component": "Button",
|
||||
"text": "Left",
|
||||
},
|
||||
"second": {
|
||||
"component": "Button",
|
||||
"text": "Right",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert_eq!(trace(&layout), expected);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user