Browse Source

feat(core/rust/ui): bitcoin layouts

[no changelog]
pull/2357/head
Martin Milata 3 months ago
parent
commit
c9ca7cd544
  1. BIN
      core/assets/error.png
  2. BIN
      core/assets/success.png
  3. BIN
      core/assets/warn.png
  4. 1
      core/embed/rust/build.rs
  5. 25
      core/embed/rust/librust_qstr.h
  6. 6
      core/embed/rust/src/micropython/buffer.rs
  7. 12
      core/embed/rust/src/micropython/map.rs
  8. 4
      core/embed/rust/src/trezorhal/display.rs
  9. 1
      core/embed/rust/src/trezorhal/mod.rs
  10. 75
      core/embed/rust/src/trezorhal/qr.rs
  11. 56
      core/embed/rust/src/ui/component/base.rs
  12. 42
      core/embed/rust/src/ui/component/image.rs
  13. 10
      core/embed/rust/src/ui/component/label.rs
  14. 4
      core/embed/rust/src/ui/component/mod.rs
  15. 28
      core/embed/rust/src/ui/component/painter.rs
  16. 35
      core/embed/rust/src/ui/display.rs
  17. 3
      core/embed/rust/src/ui/layout/obj.rs
  18. 109
      core/embed/rust/src/ui/model_tt/component/button.rs
  19. 97
      core/embed/rust/src/ui/model_tt/component/dialog.rs
  20. 12
      core/embed/rust/src/ui/model_tt/component/frame.rs
  21. 7
      core/embed/rust/src/ui/model_tt/component/mod.rs
  22. 26
      core/embed/rust/src/ui/model_tt/component/page.rs
  23. 519
      core/embed/rust/src/ui/model_tt/layout.rs
  24. BIN
      core/embed/rust/src/ui/model_tt/res/error.toif
  25. BIN
      core/embed/rust/src/ui/model_tt/res/success.toif
  26. BIN
      core/embed/rust/src/ui/model_tt/res/warn.toif
  27. 26
      core/embed/rust/src/ui/model_tt/theme.rs
  28. 118
      core/mocks/generated/trezorui2.pyi
  29. 378
      core/src/trezor/ui/layouts/tt_v2/__init__.py
  30. 966
      tests/ui_tests/fixtures.json

BIN
core/assets/error.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
core/assets/success.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
core/assets/warn.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

1
core/embed/rust/build.rs

@ -258,6 +258,7 @@ fn generate_trezorhal_bindings() {
.allowlist_function("display_bar")
.allowlist_function("display_bar_radius")
.allowlist_function("display_icon")
.allowlist_function("display_image")
.allowlist_function("display_toif_info")
.allowlist_function("display_loader")
.allowlist_function("display_pixeldata")

25
core/embed/rust/librust_qstr.h

@ -17,7 +17,18 @@ static void _librust_qstrs(void) {
MP_QSTR_CANCELLED;
MP_QSTR_INFO;
MP_QSTR_confirm_action;
MP_QSTR_confirm_blob;
MP_QSTR_confirm_coinjoin;
MP_QSTR_confirm_joint_total;
MP_QSTR_confirm_modify_fee;
MP_QSTR_confirm_modify_output;
MP_QSTR_confirm_output;
MP_QSTR_confirm_payment_request;
MP_QSTR_confirm_text;
MP_QSTR_confirm_total;
MP_QSTR_show_qr;
MP_QSTR_show_success;
MP_QSTR_show_warning;
MP_QSTR_request_pin;
MP_QSTR_request_passphrase;
MP_QSTR_request_bip39;
@ -33,14 +44,28 @@ static void _librust_qstrs(void) {
MP_QSTR_page_count;
MP_QSTR_title;
MP_QSTR_subtitle;
MP_QSTR_action;
MP_QSTR_description;
MP_QSTR_extra;
MP_QSTR_verb;
MP_QSTR_verb_cancel;
MP_QSTR_hold;
MP_QSTR_reverse;
MP_QSTR_prompt;
MP_QSTR_subprompt;
MP_QSTR_warning;
MP_QSTR_allow_cancel;
MP_QSTR_max_len;
MP_QSTR_amount_change;
MP_QSTR_amount_new;
MP_QSTR_ask_pagination;
MP_QSTR_case_sensitive;
MP_QSTR_coin_name;
MP_QSTR_max_feerate;
MP_QSTR_max_rounds;
MP_QSTR_spending_amount;
MP_QSTR_total_amount;
MP_QSTR_total_fee_new;
MP_QSTR_user_fee_change;
}

6
core/embed/rust/src/micropython/buffer.rs

@ -143,6 +143,12 @@ impl AsMut<[u8]> for BufferMut {
#[derive(Default)]
pub struct StrBuffer(Buffer);
impl StrBuffer {
pub fn empty() -> Self {
Self::from("")
}
}
impl TryFrom<Obj> for StrBuffer {
type Error = Error;

12
core/embed/rust/src/micropython/map.rs

@ -100,6 +100,18 @@ impl Map {
}
}
pub fn get_or<T>(&self, index: impl Into<Obj>, default: T) -> Result<T, Error>
where
T: TryFrom<Obj, Error = Error>,
{
let res = self.get(index);
match res {
Ok(obj) => obj.try_into(),
Err(Error::KeyError(_)) => Ok(default),
Err(e) => Err(e),
}
}
pub fn set(&mut self, index: impl Into<Obj>, value: impl Into<Obj>) -> Result<(), Error> {
self.set_obj(index.into(), value.into())
}

4
core/embed/rust/src/trezorhal/display.rs

@ -62,6 +62,10 @@ pub fn icon(x: i32, y: i32, w: i32, h: i32, data: &[u8], fgcolor: u16, bgcolor:
}
}
pub fn image(x: i32, y: i32, w: i32, h: i32, data: &[u8]) {
unsafe { ffi::display_image(x, y, w, h, data.as_ptr() as _, data.len() as _) }
}
pub fn toif_info(data: &[u8]) -> Result<ToifInfo, ()> {
let mut width: cty::uint16_t = 0;
let mut height: cty::uint16_t = 0;

1
core/embed/rust/src/trezorhal/mod.rs

@ -3,6 +3,7 @@ pub mod common;
#[cfg(feature = "ui")]
pub mod display;
mod ffi;
pub mod qr;
pub mod random;
#[cfg(feature = "model_tr")]
pub mod rgb_led;

75
core/embed/rust/src/trezorhal/qr.rs

@ -0,0 +1,75 @@
use crate::error::Error;
use cstr_core::CStr;
extern "C" {
fn display_qrcode(
x: cty::c_int,
y: cty::c_int,
data: *const cty::uint16_t,
scale: cty::uint8_t,
);
}
const NVERSIONS: usize = 10; // range of versions (=capacities) that we support
const QR_WIDTHS: [u32; NVERSIONS] = [21, 25, 29, 33, 37, 41, 45, 49, 53, 57];
const THRESHOLDS_BINARY: [usize; NVERSIONS] = [14, 26, 42, 62, 84, 106, 122, 152, 180, 213];
const THRESHOLDS_ALPHANUM: [usize; NVERSIONS] = [20, 38, 61, 90, 122, 154, 178, 221, 262, 311];
const ALPHANUM: &str = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $*+-./:";
const MAX_DATA: usize = THRESHOLDS_ALPHANUM[THRESHOLDS_ALPHANUM.len() - 1] + 1; //FIXME
fn is_alphanum_only(data: &str) -> bool {
data.chars().all(|c| ALPHANUM.contains(c))
}
fn qr_version_index(data: &str, thresholds: &[usize]) -> Option<usize> {
for (i, threshold) in thresholds.iter().enumerate() {
if data.len() <= *threshold {
return Some(i);
}
}
None
}
pub fn render_qrcode(
x: i32,
y: i32,
data: &str,
max_size: u32,
case_sensitive: bool,
) -> Result<(), Error> {
let data_len = data.len();
let version_idx;
let mut buffer = [0u8; MAX_DATA];
assert!(data_len < buffer.len());
buffer.as_mut_slice()[..data_len].copy_from_slice(data.as_bytes());
if case_sensitive && !is_alphanum_only(data) {
version_idx = match qr_version_index(data, &THRESHOLDS_BINARY) {
Some(idx) => idx,
_ => return Err(Error::OutOfRange),
}
} else if let Some(idx) = qr_version_index(data, &THRESHOLDS_ALPHANUM) {
version_idx = idx;
if data_len > THRESHOLDS_BINARY[idx] {
for c in buffer.iter_mut() {
c.make_ascii_uppercase()
}
};
} else {
return Err(Error::OutOfRange);
}
let size = QR_WIDTHS[version_idx];
let scale = max_size / size;
assert!((1..=10).contains(&scale));
unsafe {
buffer[data_len] = 0u8;
let cstr = CStr::from_bytes_with_nul_unchecked(&buffer[..data_len + 1]);
display_qrcode(x, y, cstr.as_ptr() as _, scale as u8);
Ok(())
}
}

56
core/embed/rust/src/ui/component/base.rs

@ -7,7 +7,7 @@ use crate::{
ui::{
component::{maybe::PaintOverlapping, Map},
display::Color,
geometry::Rect,
geometry::{Offset, Rect},
},
};
@ -210,6 +210,60 @@ where
}
}
impl<M, T, U, V> Component for (T, U, V)
where
T: Component<Msg = M>,
U: Component<Msg = M>,
V: Component<Msg = M>,
{
type Msg = M;
fn place(&mut self, bounds: Rect) -> Rect {
self.0
.place(bounds)
.union(self.1.place(bounds))
.union(self.2.place(bounds))
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
self.0
.event(ctx, event)
.or_else(|| self.1.event(ctx, event))
.or_else(|| self.2.event(ctx, event))
}
fn paint(&mut self) {
self.0.paint();
self.1.paint();
self.2.paint();
}
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.0.bounds(sink);
self.1.bounds(sink);
self.2.bounds(sink);
}
}
#[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,

42
core/embed/rust/src/ui/component/image.rs

@ -0,0 +1,42 @@
use crate::ui::{
component::{Component, Event, EventCtx, Never},
display,
geometry::Rect,
};
pub struct Image {
image: &'static [u8],
area: Rect,
}
impl Image {
pub fn new(image: &'static [u8]) -> Self {
Self {
image,
area: Rect::zero(),
}
}
}
impl Component for Image {
type Msg = Never;
fn place(&mut self, bounds: Rect) -> Rect {
self.area = bounds;
self.area
}
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
None
}
fn paint(&mut self) {
display::image(self.area.center(), self.image)
}
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
if let Some((size, _)) = display::toif_info(self.image) {
sink(Rect::from_center_and_size(self.area.center(), size));
}
}
}

10
core/embed/rust/src/ui/component/label.rs

@ -92,3 +92,13 @@ where
sink(self.area)
}
}
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for Label<T>
where
T: Deref<Target = str>,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.string(&self.text)
}
}

4
core/embed/rust/src/ui/component/mod.rs

@ -2,6 +2,7 @@
pub mod base;
pub mod empty;
pub mod image;
pub mod label;
pub mod map;
pub mod maybe;
@ -13,12 +14,13 @@ pub mod text;
pub use base::{Child, Component, ComponentExt, Event, EventCtx, Never, TimerToken};
pub use empty::Empty;
pub use image::Image;
pub use label::{Label, LabelStyle};
pub use map::Map;
pub use maybe::Maybe;
pub use pad::Pad;
pub use paginated::{PageMsg, Paginate};
pub use painter::Painter;
pub use painter::{qrcode_painter, Painter};
pub use placed::GridPlaced;
pub use text::{
formatted::FormattedText,

28
core/embed/rust/src/ui/component/painter.rs

@ -1,5 +1,6 @@
use crate::ui::{
component::{Component, Event, EventCtx, Never},
display,
geometry::Rect,
};
@ -35,4 +36,31 @@ where
fn paint(&mut self) {
(self.func)(self.area);
}
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(self.area)
}
}
#[cfg(feature = "ui_debug")]
impl<F> crate::trace::Trace for Painter<F> {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.string("Painter")
}
}
pub fn qrcode_painter<T>(data: T, max_size: u32, case_sensitive: bool) -> Painter<impl FnMut(Rect)>
where
T: AsRef<str>,
{
// Ignore errors as we currently can't propagate them out of paint().
let f = move |area: Rect| {
display::qrcode(area.center(), data.as_ref(), max_size, case_sensitive).unwrap_or(())
};
Painter::new(f)
}
pub fn image_painter(image: &'static [u8]) -> Painter<impl FnMut(Rect)> {
let f = move |area: Rect| display::image(area.center(), image);
Painter::new(f)
}

35
core/embed/rust/src/ui/display.rs

@ -1,7 +1,8 @@
use super::constant;
use crate::{
error::Error,
time::Duration,
trezorhal::{display, time},
trezorhal::{display, qr, time},
};
use super::geometry::{Offset, Point, Rect};
@ -91,6 +92,34 @@ pub fn icon(center: Point, data: &[u8], fg_color: Color, bg_color: Color) {
);
}
pub fn image(center: Point, data: &[u8]) {
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.into(), toif_info.height.into()),
);
display::image(
r.x0,
r.y0,
r.width(),
r.height(),
&data[12..], // Skip TOIF header.
);
}
pub fn toif_info(data: &[u8]) -> Option<(Offset, bool)> {
if let Ok(info) = display::toif_info(data) {
Some((
Offset::new(info.width.into(), info.height.into()),
info.grayscale,
))
} else {
None
}
}
// Used on T1 only.
pub fn rect_fill_rounded1(r: Rect, fg_color: Color, bg_color: Color) {
display::bar(r.x0, r.y0, r.width(), r.height(), fg_color.into());
@ -151,6 +180,10 @@ pub fn loader_indeterminate(
);
}
pub fn qrcode(center: Point, data: &str, max_size: u32, case_sensitive: bool) -> Result<(), Error> {
qr::render_qrcode(center.x, center.y, data, max_size, case_sensitive)
}
pub fn text(baseline: Point, text: &str, font: Font, fg_color: Color, bg_color: Color) {
display::text(
baseline.x,

3
core/embed/rust/src/ui/layout/obj.rs

@ -27,8 +27,7 @@ use crate::ui::event::ButtonEvent;
use crate::ui::event::TouchEvent;
/// 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`.
/// message values into MicroPython `Obj`s.
pub trait ComponentMsgObj: Component {
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error>;
}

109
core/embed/rust/src/ui/model_tt/component/button.rs

@ -340,28 +340,115 @@ pub struct ButtonStyle {
}
impl<T> Button<T> {
pub fn left_right<F0, F1, R>(
pub fn cancel_confirm(
left: Button<T>,
left_map: F0,
right: Button<T>,
right_map: F1,
) -> (Map<GridPlaced<Self>, F0>, Map<GridPlaced<Self>, F1>)
right_size_factor: usize,
) -> CancelConfirm<
T,
impl Fn(ButtonMsg) -> Option<CancelConfirmMsg>,
impl Fn(ButtonMsg) -> Option<CancelConfirmMsg>,
>
where
F0: Fn(ButtonMsg) -> Option<R>,
F1: Fn(ButtonMsg) -> Option<R>,
T: AsRef<str>,
{
let columns = 1 + right_size_factor;
(
GridPlaced::new(left)
.with_grid(1, 3)
.with_grid(1, columns)
.with_spacing(theme::BUTTON_SPACING)
.with_row_col(0, 0)
.map(left_map),
.map(|msg| {
(matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Cancelled)
}),
GridPlaced::new(right)
.with_grid(1, 3)
.with_grid(1, columns)
.with_spacing(theme::BUTTON_SPACING)
.with_from_to((0, 1), (0, 2))
.map(right_map),
.with_from_to((0, 1), (0, right_size_factor))
.map(|msg| {
(matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Confirmed)
}),
)
}
pub fn cancel_confirm_text(
left: Option<T>,
right: T,
) -> CancelConfirm<
T,
impl Fn(ButtonMsg) -> Option<CancelConfirmMsg>,
impl Fn(ButtonMsg) -> Option<CancelConfirmMsg>,
>
where
T: AsRef<str>,
{
let (left, right_size_factor) = if let Some(verb) = left {
(Button::with_text(verb), 1)
} else {
(Button::with_icon(theme::ICON_CANCEL), 2)
};
let right = Button::with_text(right).styled(theme::button_confirm());
Self::cancel_confirm(left, right, right_size_factor)
}
pub fn cancel_info_confirm(
confirm: T,
info: T,
) -> CancelInfoConfirm<
T,
impl Fn(ButtonMsg) -> Option<CancelInfoConfirmMsg>,
impl Fn(ButtonMsg) -> Option<CancelInfoConfirmMsg>,
impl Fn(ButtonMsg) -> Option<CancelInfoConfirmMsg>,
>
where
T: AsRef<str>,
{
let right = Button::with_text(confirm).styled(theme::button_confirm());
let top = Button::with_text(info);
let left = Button::with_icon(theme::ICON_CANCEL);
(
GridPlaced::new(left)
.with_grid(2, 3)
.with_spacing(theme::BUTTON_SPACING)
.with_row_col(1, 0)
.map(|msg| {
(matches!(msg, ButtonMsg::Clicked)).then(|| CancelInfoConfirmMsg::Cancelled)
}),
GridPlaced::new(top)
.with_grid(2, 3)
.with_spacing(theme::BUTTON_SPACING)
.with_from_to((0, 0), (0, 2))
.map(|msg| (matches!(msg, ButtonMsg::Clicked)).then(|| CancelInfoConfirmMsg::Info)),
GridPlaced::new(right)
.with_grid(2, 3)
.with_spacing(theme::BUTTON_SPACING)
.with_from_to((1, 1), (1, 2))
.map(|msg| {
(matches!(msg, ButtonMsg::Clicked)).then(|| CancelInfoConfirmMsg::Confirmed)
}),
)
}
}
type CancelConfirm<T, F0, F1> = (
Map<GridPlaced<Button<T>>, F0>,
Map<GridPlaced<Button<T>>, F1>,
);
pub enum CancelConfirmMsg {
Cancelled,
Confirmed,
}
type CancelInfoConfirm<T, F0, F1, F2> = (
Map<GridPlaced<Button<T>>, F0>,
Map<GridPlaced<Button<T>>, F1>,
Map<GridPlaced<Button<T>>, F2>,
);
pub enum CancelInfoConfirmMsg {
Cancelled,
Info,
Confirmed,
}

97
core/embed/rust/src/ui/model_tt/component/dialog.rs

@ -1,6 +1,8 @@
use core::ops::Deref;
use crate::ui::{
component::{Child, Component, Event, EventCtx},
geometry::{Grid, Rect},
component::{Child, Component, Event, EventCtx, Image, Label, Never},
geometry::{Grid, Insets, Rect},
};
use super::{theme, Button};
@ -95,3 +97,94 @@ where
t.close();
}
}
pub struct IconDialog<T, U> {
image: Child<Image>,
title: Label<T>,
description: Option<Label<T>>,
controls: Child<U>,
}
impl<T, U> IconDialog<T, U>
where
T: Deref<Target = str>,
U: Component,
{
pub fn new(icon: &'static [u8], title: T, controls: U) -> Self {
Self {
image: Child::new(Image::new(icon)),
title: Label::centered(title, theme::label_warning()),
description: None,
controls: Child::new(controls),
}
}
pub fn with_description(mut self, description: T) -> Self {
self.description = Some(Label::centered(description, theme::label_warning_value()));
self
}
pub const ICON_AREA_HEIGHT: i32 = 64;
pub const DESCRIPTION_SPACE: i32 = 13;
pub const VALUE_SPACE: i32 = 9;
}
impl<T, U> Component for IconDialog<T, U>
where
T: Deref<Target = str>,
U: Component,
{
type Msg = DialogMsg<Never, U::Msg>;
fn place(&mut self, bounds: Rect) -> Rect {
let bounds = bounds.inset(theme::borders());
let (content, buttons) = bounds.split_bottom(Button::<&str>::HEIGHT);
let (image, content) = content.split_top(Self::ICON_AREA_HEIGHT);
let content = content.inset(Insets::top(Self::DESCRIPTION_SPACE));
let (title, content) = content.split_top(self.title.size().y);
let content = content.inset(Insets::top(Self::VALUE_SPACE));
self.image.place(image);
self.title.place(title);
self.description.place(content);
self.controls.place(buttons);
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
self.title.event(ctx, event);
self.description.event(ctx, event);
self.controls.event(ctx, event).map(Self::Msg::Controls)
}
fn paint(&mut self) {
self.image.paint();
self.title.paint();
self.description.paint();
self.controls.paint();
}
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.image.bounds(sink);
self.title.bounds(sink);
self.description.bounds(sink);
self.controls.bounds(sink);
}
}
#[cfg(feature = "ui_debug")]
impl<T, U> crate::trace::Trace for IconDialog<T, U>
where
T: Deref<Target = str>,
U: crate::trace::Trace,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("IconDialog");
t.field("title", &self.title);
if let Some(ref description) = self.description {
t.field("description", description);
}
t.field("controls", &self.controls);
t.close();
}
}

12
core/embed/rust/src/ui/model_tt/component/frame.rs

@ -7,6 +7,7 @@ use crate::ui::{
pub struct Frame<T, U> {
area: Rect,
border: Insets,
title: U,
content: Child<T>,
}
@ -20,10 +21,16 @@ where
Self {
title,
area: Rect::zero(),
border: theme::borders_scroll(),
content: Child::new(content),
}
}
pub fn with_border(mut self, border: Insets) -> Self {
self.border = border;
self
}
pub fn inner(&self) -> &T {
self.content.inner()
}
@ -37,11 +44,10 @@ where
type Msg = T::Msg;
fn place(&mut self, bounds: Rect) -> Rect {
// Same as PageLayout::BUTTON_SPACE.
const TITLE_SPACE: i32 = 6;
const TITLE_SPACE: i32 = theme::BUTTON_SPACING;
let (title_area, content_area) = bounds
.inset(theme::borders_scroll())
.inset(self.border)
.split_top(theme::FONT_BOLD.text_height());
let title_area = title_area.inset(Insets::left(theme::CONTENT_BORDER));
let content_area = content_area.inset(Insets::top(TITLE_SPACE));

7
core/embed/rust/src/ui/model_tt/component/mod.rs

@ -8,8 +8,11 @@ mod page;
mod scroll;
mod swipe;
pub use button::{Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet};
pub use dialog::{Dialog, DialogLayout, DialogMsg};
pub use button::{
Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet, CancelConfirmMsg,
CancelInfoConfirmMsg,
};
pub use dialog::{Dialog, DialogLayout, DialogMsg, IconDialog};
pub use frame::Frame;
pub use hold_to_confirm::{HoldToConfirm, HoldToConfirmMsg};
pub use keyboard::{

26
core/embed/rust/src/ui/model_tt/component/page.rs

@ -8,7 +8,7 @@ use crate::ui::{
use super::{
hold_to_confirm::{handle_hold_event, CancelHold, CancelHoldMsg},
theme, Button, Loader, ScrollBar, Swipe, SwipeDirection,
theme, Button, CancelConfirmMsg, Loader, ScrollBar, Swipe, SwipeDirection,
};
pub struct SwipePage<T, U> {
@ -19,6 +19,7 @@ pub struct SwipePage<T, U> {
scrollbar: ScrollBar,
hint: Label<&'static str>,
fade: Option<i32>,
button_rows: i32,
}
impl<T, U> SwipePage<T, U>
@ -36,9 +37,15 @@ where
pad: Pad::with_background(background),
hint: Label::centered("SWIPE TO CONTINUE", theme::label_page_hint()),
fade: None,
button_rows: 1,
}
}
pub fn with_button_rows(mut self, rows: usize) -> Self {
self.button_rows = rows as i32;
self
}
fn setup_swipe(&mut self) {
self.swipe.allow_up = self.scrollbar.has_next_page();
self.swipe.allow_down = self.scrollbar.has_previous_page();
@ -69,7 +76,7 @@ where
type Msg = PageMsg<T::Msg, U::Msg>;
fn place(&mut self, bounds: Rect) -> Rect {
let layout = PageLayout::new(bounds);
let layout = PageLayout::new(bounds, self.button_rows);
self.pad.place(bounds);
self.swipe.place(bounds);
self.hint.place(layout.hint);
@ -186,15 +193,16 @@ pub struct PageLayout {
}
impl PageLayout {
const BUTTON_SPACE: i32 = 6;
const SCROLLBAR_WIDTH: i32 = 10;
const SCROLLBAR_SPACE: i32 = 10;
const HINT_OFF: i32 = 19;
pub fn new(area: Rect) -> Self {
let (content, buttons) = area.split_bottom(Button::<&str>::HEIGHT);
pub fn new(area: Rect, button_rows: i32) -> Self {
let buttons_height = button_rows * Button::<&str>::HEIGHT
+ button_rows.saturating_sub(1) * theme::BUTTON_SPACING;
let (content, buttons) = area.split_bottom(buttons_height);
let (_, hint) = area.split_bottom(Self::HINT_OFF);
let (content, _space) = content.split_bottom(Self::BUTTON_SPACE);
let (content, _space) = content.split_bottom(theme::BUTTON_SPACING);
let (buttons, _space) = buttons.split_right(theme::CONTENT_BORDER);
let (_space, content) = content.split_left(theme::CONTENT_BORDER);
let (content_single_page, _space) = content.split_right(theme::CONTENT_BORDER);
@ -236,7 +244,7 @@ where
T: Paginate,
T: Component,
{
type Msg = PageMsg<T::Msg, bool>;
type Msg = PageMsg<T::Msg, CancelConfirmMsg>;
fn place(&mut self, bounds: Rect) -> Rect {
self.inner.place(bounds);
@ -249,7 +257,7 @@ where
let button_msg = match msg {
Some(PageMsg::Content(c)) => return Some(PageMsg::Content(c)),
Some(PageMsg::Controls(CancelHoldMsg::Cancelled)) => {
return Some(PageMsg::Controls(false))
return Some(PageMsg::Controls(CancelConfirmMsg::Cancelled))
}
Some(PageMsg::Controls(CancelHoldMsg::HoldButton(b))) => Some(b),
_ => None,
@ -262,7 +270,7 @@ where
&mut self.inner.pad,
&mut self.inner.content,
) {
return Some(PageMsg::Controls(true));
return Some(PageMsg::Controls(CancelConfirmMsg::Confirmed));
}
None
}

519
core/embed/rust/src/ui/model_tt/layout.rs

@ -2,14 +2,25 @@ use core::{convert::TryInto, ops::Deref};
use crate::{
error::Error,
micropython::{buffer::StrBuffer, map::Map, module::Module, obj::Obj, qstr::Qstr, util},
micropython::{
buffer::StrBuffer,
iter::{Iter, IterBuf},
map::Map,
module::Module,
obj::Obj,
qstr::Qstr,
util,
},
ui::{
component::{
self,
base::ComponentExt,
paginated::{PageMsg, Paginate},
painter,
text::paragraphs::Paragraphs,
Component,
},
geometry,
layout::{
obj::{ComponentMsgObj, LayoutObj},
result::{CANCELLED, CONFIRMED, INFO},
@ -19,23 +30,61 @@ use crate::{
use super::{
component::{
Bip39Input, Button, ButtonMsg, Dialog, DialogMsg, Frame, HoldToConfirm, HoldToConfirmMsg,
MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg, PassphraseKeyboard,
PassphraseKeyboardMsg, PinKeyboard, PinKeyboardMsg, Slip39Input, SwipeHoldPage, SwipePage,
Bip39Input, Button, ButtonMsg, CancelConfirmMsg, CancelInfoConfirmMsg, Dialog, DialogMsg,
Frame, HoldToConfirm, HoldToConfirmMsg, IconDialog, MnemonicInput, MnemonicKeyboard,
MnemonicKeyboardMsg, PassphraseKeyboard, PassphraseKeyboardMsg, PinKeyboard,
PinKeyboardMsg, Slip39Input, SwipeHoldPage, SwipePage,
},
theme,
};
impl TryFrom<CancelConfirmMsg> for Obj {
type Error = Error;
fn try_from(value: CancelConfirmMsg) -> Result<Self, Self::Error> {
match value {
CancelConfirmMsg::Cancelled => Ok(CANCELLED.as_obj()),
CancelConfirmMsg::Confirmed => Ok(CONFIRMED.as_obj()),
}
}
}
impl TryFrom<CancelInfoConfirmMsg> for Obj {
type Error = Error;
fn try_from(value: CancelInfoConfirmMsg) -> Result<Self, Self::Error> {
match value {
CancelInfoConfirmMsg::Cancelled => Ok(CANCELLED.as_obj()),
CancelInfoConfirmMsg::Info => Ok(INFO.as_obj()),
CancelInfoConfirmMsg::Confirmed => Ok(CONFIRMED.as_obj()),
}
}
}
impl<T, U> ComponentMsgObj for Dialog<T, U>
where
T: ComponentMsgObj,
U: Component<Msg = bool>,
U: Component,
<U as Component>::Msg: TryInto<Obj, Error = Error>,
{
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
DialogMsg::Content(c) => Ok(self.inner().msg_try_into_obj(c)?),
DialogMsg::Controls(false) => Ok(CANCELLED.as_obj()),
DialogMsg::Controls(true) => Ok(CONFIRMED.as_obj()),
DialogMsg::Controls(msg) => msg.try_into(),
}
}
}
impl<T, U> ComponentMsgObj for IconDialog<T, U>
where
T: Deref<Target = str>,
U: Component,
<U as Component>::Msg: TryInto<Obj, Error = Error>,
{
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
DialogMsg::Controls(msg) => msg.try_into(),
_ => unreachable!(),
}
}
}
@ -105,13 +154,13 @@ where
impl<T, U> ComponentMsgObj for SwipePage<T, U>
where
T: Component + Paginate,
U: Component<Msg = bool>,
U: Component,
<U as Component>::Msg: TryInto<Obj, Error = Error>,
{
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
PageMsg::Content(_) => Err(Error::TypeError),
PageMsg::Controls(true) => Ok(CONFIRMED.as_obj()),
PageMsg::Controls(false) => Ok(CANCELLED.as_obj()),
PageMsg::Controls(msg) => msg.try_into(),
}
}
}
@ -123,20 +172,33 @@ where
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
PageMsg::Content(_) => Err(Error::TypeError),
PageMsg::Controls(true) => Ok(CONFIRMED.as_obj()),
PageMsg::Controls(false) => Ok(CANCELLED.as_obj()),
PageMsg::Controls(msg) => msg.try_into(),
}
}
}
impl<F> ComponentMsgObj for painter::Painter<F>
where
F: FnMut(geometry::Rect),
{
fn msg_try_into_obj(&self, _msg: Self::Msg) -> Result<Obj, Error> {
unreachable!()
}
}
extern "C" fn new_confirm_action(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let action: Option<StrBuffer> = kwargs.get(Qstr::MP_QSTR_action)?.try_into_option()?;
let description: Option<StrBuffer> =
kwargs.get(Qstr::MP_QSTR_description)?.try_into_option()?;
let verb: Option<StrBuffer> = kwargs.get(Qstr::MP_QSTR_verb)?.try_into_option()?;
let reverse: bool = kwargs.get(Qstr::MP_QSTR_reverse)?.try_into()?;
let verb: StrBuffer = kwargs.get_or(Qstr::MP_QSTR_verb, "CONFIRM".into())?;
let verb_cancel: Option<StrBuffer> = kwargs
.get(Qstr::MP_QSTR_verb_cancel)
.unwrap_or_else(|_| Obj::const_none())
.try_into_option()?;
let reverse: bool = kwargs.get_or(Qstr::MP_QSTR_reverse, false)?;
let hold: bool = kwargs.get_or(Qstr::MP_QSTR_hold, false)?;
let paragraphs = {
let action = action.unwrap_or_default();
@ -154,12 +216,99 @@ extern "C" fn new_confirm_action(n_args: usize, args: *const Obj, kwargs: *mut M
paragraphs
};
let buttons = Button::left_right(
let obj = if hold {
LayoutObj::new(
Frame::new(title, SwipeHoldPage::new(paragraphs, theme::BG)).into_child(),
)?
} else {
let buttons = Button::cancel_confirm_text(verb_cancel, verb);
LayoutObj::new(
Frame::new(title, SwipePage::new(paragraphs, buttons, theme::BG)).into_child(),
)?
};
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_confirm_blob(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let data: StrBuffer = kwargs.get(Qstr::MP_QSTR_data)?.try_into()?;
let description: StrBuffer =
kwargs.get_or(Qstr::MP_QSTR_description, StrBuffer::empty())?;
let extra: StrBuffer = kwargs.get_or(Qstr::MP_QSTR_extra, StrBuffer::empty())?;
let verb_cancel: Option<StrBuffer> = kwargs
.get(Qstr::MP_QSTR_verb_cancel)
.unwrap_or_else(|_| Obj::const_none())
.try_into_option()?;
let _ask_pagination: bool = kwargs.get_or(Qstr::MP_QSTR_ask_pagination, false)?;
let hold: bool = kwargs.get_or(Qstr::MP_QSTR_hold, false)?;
let paragraphs = Paragraphs::new()
.add::<theme::TTDefaultText>(theme::FONT_NORMAL, description)
.add::<theme::TTDefaultText>(theme::FONT_BOLD, extra)
.add::<theme::TTDefaultText>(theme::FONT_MONO, data);
let obj = if hold {
LayoutObj::new(
Frame::new(title, SwipeHoldPage::new(paragraphs, theme::BG)).into_child(),
)?
} else {
let buttons = Button::cancel_confirm_text(verb_cancel, "CONFIRM".into());
LayoutObj::new(
Frame::new(title, SwipePage::new(paragraphs, buttons, theme::BG)).into_child(),
)?
};
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_show_qr(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let address: StrBuffer = kwargs.get(Qstr::MP_QSTR_address)?.try_into()?;
let verb_cancel: StrBuffer = kwargs.get(Qstr::MP_QSTR_verb_cancel)?.try_into()?;
let case_sensitive: bool = kwargs.get(Qstr::MP_QSTR_case_sensitive)?.try_into()?;
let buttons = Button::cancel_confirm(
Button::with_text(verb_cancel),
Button::with_text("CONFIRM".into()).styled(theme::button_confirm()),
1,
);
let obj = LayoutObj::new(
Frame::new(
title,
Dialog::new(
painter::qrcode_painter(address, theme::QR_SIDE_MAX, case_sensitive),
buttons,
),
)
.with_border(theme::borders())
.into_child(),
)?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_confirm_output(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let description: StrBuffer = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?;
let value: StrBuffer = kwargs.get(Qstr::MP_QSTR_value)?.try_into()?;
let verb = "NEXT";
let paragraphs = Paragraphs::new()
.add::<theme::TTDefaultText>(theme::FONT_NORMAL, description)
.add::<theme::TTDefaultText>(theme::FONT_MONO, value);
let buttons = Button::cancel_confirm(
Button::with_icon(theme::ICON_CANCEL),
|msg| (matches!(msg, ButtonMsg::Clicked)).then(|| false),
Button::with_text(verb.unwrap_or_else(|| "CONFIRM".into()))
.styled(theme::button_confirm()),
|msg| (matches!(msg, ButtonMsg::Clicked)).then(|| true),
Button::with_text(verb).styled(theme::button_confirm()),
2,
);
let obj = LayoutObj::new(
@ -170,15 +319,226 @@ extern "C" fn new_confirm_action(n_args: usize, args: *const Obj, kwargs: *mut M
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_confirm_total(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let description: StrBuffer = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?;
let value: StrBuffer = kwargs.get(Qstr::MP_QSTR_value)?.try_into()?;
let paragraphs = Paragraphs::new()
.add::<theme::TTDefaultText>(theme::FONT_NORMAL, description)
.add::<theme::TTDefaultText>(theme::FONT_MONO, value);
let obj = LayoutObj::new(
Frame::new(title, SwipeHoldPage::new(paragraphs, theme::BG)).into_child(),
)?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_confirm_joint_total(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let spending_amount: StrBuffer = kwargs.get(Qstr::MP_QSTR_spending_amount)?.try_into()?;
let total_amount: StrBuffer = kwargs.get(Qstr::MP_QSTR_total_amount)?.try_into()?;
let paragraphs = Paragraphs::new()
.add::<theme::TTDefaultText>(theme::FONT_NORMAL, "You are contributing:".into())
.add::<theme::TTDefaultText>(theme::FONT_MONO, spending_amount)
.add::<theme::TTDefaultText>(theme::FONT_NORMAL, "To the total amount:".into())
.add::<theme::TTDefaultText>(theme::FONT_MONO, total_amount);
let obj = LayoutObj::new(
Frame::new(
"JOINT TRANSACTION",
SwipeHoldPage::new(paragraphs, theme::BG),
)
.into_child(),
)?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_confirm_modify_output(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let address: StrBuffer = kwargs.get(Qstr::MP_QSTR_address)?.try_into()?;
let sign: i32 = kwargs.get(Qstr::MP_QSTR_sign)?.try_into()?;
let amount_change: StrBuffer = kwargs.get(Qstr::MP_QSTR_amount_change)?.try_into()?;
let amount_new: StrBuffer = kwargs.get(Qstr::MP_QSTR_amount_new)?.try_into()?;
let description = if sign < 0 {
"Decrease amount by:"
} else {
"Increase amount by:"
};
let paragraphs = Paragraphs::new()
.add::<theme::TTDefaultText>(theme::FONT_NORMAL, "Address:".into())
.add::<theme::TTDefaultText>(theme::FONT_MONO, address)
// FIXME pagebreak
.add::<theme::TTDefaultText>(theme::FONT_NORMAL, description.into())
.add::<theme::TTDefaultText>(theme::FONT_MONO, amount_change)
.add::<theme::TTDefaultText>(theme::FONT_NORMAL, "New amount:".into())
.add::<theme::TTDefaultText>(theme::FONT_MONO, amount_new);
let buttons = Button::cancel_confirm(
Button::with_icon(theme::ICON_CANCEL),
Button::with_text("NEXT").styled(theme::button_confirm()),