1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-11-22 07:28:10 +00:00

feat(core/rust/ui): bitcoin layouts

[no changelog]
This commit is contained in:
Martin Milata 2022-05-24 12:54:02 +02:00
parent dd9a7d30e5
commit c9ca7cd544
30 changed files with 1976 additions and 584 deletions

BIN
core/assets/error.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
core/assets/success.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
core/assets/warn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()),
2,
);
let obj = LayoutObj::new(
Frame::new(
"MODIFY AMOUNT",
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_modify_fee(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let sign: i32 = kwargs.get(Qstr::MP_QSTR_sign)?.try_into()?;
let user_fee_change: StrBuffer = kwargs.get(Qstr::MP_QSTR_user_fee_change)?.try_into()?;
let total_fee_new: StrBuffer = kwargs.get(Qstr::MP_QSTR_total_fee_new)?.try_into()?;
let (description, change) = match sign {
s if s < 0 => ("Decrease your fee by:", user_fee_change),
s if s > 0 => ("Increase your fee by:", user_fee_change),
_ => ("Your fee did not change.", StrBuffer::empty()),
};
let paragraphs = Paragraphs::new()
.add::<theme::TTDefaultText>(theme::FONT_NORMAL, description.into())
.add::<theme::TTDefaultText>(theme::FONT_MONO, change)
.add::<theme::TTDefaultText>(theme::FONT_NORMAL, "\nTransaction fee:".into())
.add::<theme::TTDefaultText>(theme::FONT_MONO, total_fee_new);
let buttons = Button::cancel_confirm(
Button::with_icon(theme::ICON_CANCEL),
Button::with_text("NEXT").styled(theme::button_confirm()),
2,
);
let obj = LayoutObj::new(
Frame::new("MODIFY FEE", 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_warning(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_or(Qstr::MP_QSTR_description, StrBuffer::empty())?;
let buttons = Button::cancel_confirm(
Button::with_icon(theme::ICON_CANCEL).styled(theme::button_cancel()),
Button::with_text("CONTINUE").styled(theme::button_reset()),
2,
);
let obj = LayoutObj::new(
IconDialog::new(theme::IMAGE_WARN, title, buttons).with_description(description),
)?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_show_success(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_or(Qstr::MP_QSTR_description, StrBuffer::empty())?;
let button: StrBuffer = kwargs.get(Qstr::MP_QSTR_button)?.try_into()?;
let buttons = component::Map::new(
Button::with_text(button).styled(theme::button_confirm()),
|msg| (matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Confirmed),
);
let obj = LayoutObj::new(
IconDialog::new(theme::IMAGE_SUCCESS, title, buttons).with_description(description),
)?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_confirm_payment_request(
n_args: usize,
args: *const Obj,
kwargs: *mut Map,
) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let description: StrBuffer = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?;
let memos: Obj = kwargs.get(Qstr::MP_QSTR_memos)?;
let mut paragraphs =
Paragraphs::new().add::<theme::TTDefaultText>(theme::FONT_NORMAL, description);
let mut iter_buf = IterBuf::new();
let iter = Iter::try_from_obj_with_buf(memos, &mut iter_buf)?;
for memo in iter {
let text: StrBuffer = memo.try_into()?;
paragraphs = paragraphs.add::<theme::TTDefaultText>(theme::FONT_NORMAL, text);
}
let buttons = Button::cancel_info_confirm("CONFIRM", "DETAILS");
let obj = LayoutObj::new(
Frame::new(
"SENDING",
SwipePage::new(paragraphs, buttons, theme::BG).with_button_rows(2),
)
.into_child(),
)?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_confirm_coinjoin(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let coin_name: StrBuffer = kwargs.get(Qstr::MP_QSTR_coin_name)?.try_into()?;
let max_rounds: StrBuffer = kwargs.get(Qstr::MP_QSTR_max_rounds)?.try_into()?;
let max_feerate: StrBuffer = kwargs.get(Qstr::MP_QSTR_max_feerate)?.try_into()?;
let paragraphs = Paragraphs::new()
.add::<theme::TTDefaultText>(theme::FONT_NORMAL, "Coin name:".into())
.add::<theme::TTDefaultText>(theme::FONT_BOLD, coin_name)
.add::<theme::TTDefaultText>(theme::FONT_NORMAL, "Maximum rounds:".into())
.add::<theme::TTDefaultText>(theme::FONT_BOLD, max_rounds)
.add::<theme::TTDefaultText>(theme::FONT_NORMAL, "Maximum mining fee:".into())
.add::<theme::TTDefaultText>(theme::FONT_BOLD, max_feerate);
let obj = LayoutObj::new(
Frame::new(
"AUTHORIZE COINJOIN",
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_request_pin(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?;
let subprompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_subprompt)?.try_into()?;
let allow_cancel: Option<bool> =
kwargs.get(Qstr::MP_QSTR_allow_cancel)?.try_into_option()?;
let allow_cancel: bool = kwargs.get_or(Qstr::MP_QSTR_allow_cancel, true)?;
let warning: Option<StrBuffer> = kwargs.get(Qstr::MP_QSTR_warning)?.try_into_option()?;
let obj = LayoutObj::new(
PinKeyboard::new(prompt, subprompt, warning, allow_cancel.unwrap_or(true)).into_child(),
PinKeyboard::new(prompt, subprompt, warning, allow_cancel).into_child(),
)?;
Ok(obj.into())
};
@ -233,16 +593,119 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// description: str | None = None,
/// verb: str | None = None,
/// verb_cancel: str | None = None,
/// hold: bool | None = None,
/// hold: bool = False,
/// reverse: bool = False,
/// ) -> object:
/// """Confirm action."""
Qstr::MP_QSTR_confirm_action => obj_fn_kw!(0, new_confirm_action).as_obj(),
/// def confirm_blob(
/// *,
/// title: str,
/// data: str,
/// description: str = "",
/// extra: str = "",
/// verb_cancel: str | None = None,
/// ask_pagination: bool = False,
/// hold: bool = False,
/// ) -> object:
/// """Confirm byte sequence data."""
Qstr::MP_QSTR_confirm_blob => obj_fn_kw!(0, new_confirm_blob).as_obj(),
/// def show_qr(
/// *,
/// title: str,
/// address: str,
/// verb_cancel: str,
/// case_sensitive: bool,
/// ) -> object:
/// """Show QR code."""
Qstr::MP_QSTR_show_qr => obj_fn_kw!(0, new_show_qr).as_obj(),
/// def confirm_output(
/// *,
/// title: str,
/// description: str,
/// value: str,
/// verb: str = "NEXT",
/// ) -> object:
/// """Confirm output."""
Qstr::MP_QSTR_confirm_output => obj_fn_kw!(0, new_confirm_output).as_obj(),
/// def confirm_total(
/// *,
/// title: str,
/// description: str,
/// value: str,
/// ) -> object:
/// """Confirm total."""
Qstr::MP_QSTR_confirm_total => obj_fn_kw!(0, new_confirm_total).as_obj(),
/// def confirm_joint_total(
/// *,
/// spending_amount: str,
/// total_amount: str,
/// ) -> object:
/// """Confirm total if there are external inputs."""
Qstr::MP_QSTR_confirm_joint_total => obj_fn_kw!(0, new_confirm_joint_total).as_obj(),
/// def confirm_modify_output(
/// *,
/// address: str,
/// sign: int,
/// amount_change: str,
/// amount_new: str,
/// ) -> object:
/// """Decrease or increase amount for given address."""
Qstr::MP_QSTR_confirm_modify_output => obj_fn_kw!(0, new_confirm_modify_output).as_obj(),
/// def confirm_modify_fee(
/// *,
/// sign: int,
/// user_fee_change: str,
/// total_fee_new: str,
/// ) -> object:
/// """Decrease or increase transaction fee."""
Qstr::MP_QSTR_confirm_modify_fee => obj_fn_kw!(0, new_confirm_modify_fee).as_obj(),
/// def show_warning(
/// *,
/// title: str,
/// description: str = "",
/// ) -> object:
/// """Warning modal."""
Qstr::MP_QSTR_show_warning => obj_fn_kw!(0, new_show_warning).as_obj(),
/// def show_success(
/// *,
/// title: str,
/// button: str,
/// description: str = "",
/// ) -> object:
/// """Success modal."""
Qstr::MP_QSTR_show_success => obj_fn_kw!(0, new_show_success).as_obj(),
/// def confirm_payment_request(
/// *,
/// description: str,
/// memos: Iterable[str],
/// ) -> object:
/// """Confirm payment request."""
Qstr::MP_QSTR_confirm_payment_request => obj_fn_kw!(0, new_confirm_payment_request).as_obj(),
/// def confirm_coinjoin(
/// *,
/// coin_name: str,
/// max_rounds: str,
/// max_feerate: str,
/// ) -> object:
/// """Confirm coinjoin authorization."""
Qstr::MP_QSTR_confirm_coinjoin => obj_fn_kw!(0, new_confirm_coinjoin).as_obj(),
/// def request_pin(
/// *,
/// prompt: str,
/// subprompt: str | None = None,
/// subprompt: str,
/// allow_cancel: bool = True,
/// warning: str | None = None,
/// ) -> str | object:
@ -295,12 +758,8 @@ mod tests {
#[test]
fn trace_example_layout() {
let buttons = Button::left_right(
Button::with_text("Left"),
|msg| (matches!(msg, ButtonMsg::Clicked)).then(|| false),
Button::with_text("Right"),
|msg| (matches!(msg, ButtonMsg::Clicked)).then(|| true),
);
let buttons =
Button::cancel_confirm(Button::with_text("Left"), Button::with_text("Right"), 1);
let mut layout = Dialog::new(
FormattedText::new::<theme::TTDefaultText>(
"Testing text layout, with some text, and some more text. And {param}",

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -39,10 +39,13 @@ pub const GREY_DARK: Color = Color::rgb(51, 51, 51); // greyer
// Commonly used corner radius (i.e. for buttons).
pub const RADIUS: u8 = 2;
// Full-size QR code.
pub const QR_SIDE_MAX: u32 = 140;
// Size of icons in the UI (i.e. inside buttons).
pub const ICON_SIZE: i32 = 16;
// UI icons.
// UI icons (greyscale).
pub const ICON_CANCEL: &[u8] = include_res!("model_tt/res/cancel.toif");
pub const ICON_CONFIRM: &[u8] = include_res!("model_tt/res/confirm.toif");
pub const ICON_SPACE: &[u8] = include_res!("model_tt/res/space.toif");
@ -50,6 +53,11 @@ pub const ICON_BACK: &[u8] = include_res!("model_tt/res/back.toif");
pub const ICON_CLICK: &[u8] = include_res!("model_tt/res/click.toif");
pub const ICON_NEXT: &[u8] = include_res!("model_tt/res/next.toif");
// Large, color icons.
pub const IMAGE_WARN: &[u8] = include_res!("model_tt/res/warn.toif");
pub const IMAGE_SUCCESS: &[u8] = include_res!("model_tt/res/success.toif");
pub const IMAGE_ERROR: &[u8] = include_res!("model_tt/res/error.toif");
// Scrollbar/PIN dots.
pub const DOT_ACTIVE: &[u8] = include_res!("model_tt/res/scroll-active.toif");
pub const DOT_INACTIVE: &[u8] = include_res!("model_tt/res/scroll-inactive.toif");
@ -95,6 +103,22 @@ pub fn label_page_hint() -> LabelStyle {
}
}
pub fn label_warning() -> LabelStyle {
LabelStyle {
font: FONT_MEDIUM,
text_color: FG,
background_color: BG,
}
}
pub fn label_warning_value() -> LabelStyle {
LabelStyle {
font: FONT_NORMAL,
text_color: OFF_WHITE,
background_color: BG,
}
}
pub fn button_default() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {

View File

@ -64,17 +64,131 @@ def confirm_action(
description: str | None = None,
verb: str | None = None,
verb_cancel: str | None = None,
hold: bool | None = None,
hold: bool = False,
reverse: bool = False,
) -> object:
"""Confirm action."""
# rust/src/ui/model_tt/layout.rs
def confirm_blob(
*,
title: str,
data: str,
description: str = "",
extra: str = "",
verb_cancel: str | None = None,
ask_pagination: bool = False,
hold: bool = False,
) -> object:
"""Confirm byte sequence data."""
# rust/src/ui/model_tt/layout.rs
def show_qr(
*,
title: str,
address: str,
verb_cancel: str,
case_sensitive: bool,
) -> object:
"""Show QR code."""
# rust/src/ui/model_tt/layout.rs
def confirm_output(
*,
title: str,
description: str,
value: str,
verb: str = "NEXT",
) -> object:
"""Confirm output."""
# rust/src/ui/model_tt/layout.rs
def confirm_total(
*,
title: str,
description: str,
value: str,
) -> object:
"""Confirm total."""
# rust/src/ui/model_tt/layout.rs
def confirm_joint_total(
*,
spending_amount: str,
total_amount: str,
) -> object:
"""Confirm total if there are external inputs."""
# rust/src/ui/model_tt/layout.rs
def confirm_modify_output(
*,
address: str,
sign: int,
amount_change: str,
amount_new: str,
) -> object:
"""Decrease or increase amount for given address."""
# rust/src/ui/model_tt/layout.rs
def confirm_modify_fee(
*,
sign: int,
user_fee_change: str,
total_fee_new: str,
) -> object:
"""Decrease or increase transaction fee."""
# rust/src/ui/model_tt/layout.rs
def show_warning(
*,
title: str,
description: str = "",
) -> object:
"""Warning modal."""
# rust/src/ui/model_tt/layout.rs
def show_success(
*,
title: str,
button: str,
description: str = "",
) -> object:
"""Success modal."""
# rust/src/ui/model_tt/layout.rs
def confirm_payment_request(
*,
description: str,
memos: Iterable[str],
) -> object:
"""Confirm payment request."""
# rust/src/ui/model_tt/layout.rs
def confirm_coinjoin(
*,
coin_name: str,
max_rounds: str,
max_feerate: str,
) -> object:
"""Confirm coinjoin authorization."""
# rust/src/ui/model_tt/layout.rs
def request_pin(
*,
prompt: str,
subprompt: str | None = None,
subprompt: str,
allow_cancel: bool = True,
warning: str | None = None,
) -> str | object:

View File

@ -1,4 +1,5 @@
from typing import TYPE_CHECKING
from ubinascii import hexlify
from trezor import io, log, loop, ui, wire, workflow
from trezor.enums import ButtonRequestType
@ -120,20 +121,16 @@ async def confirm_action(
) -> None:
if isinstance(verb, bytes) or isinstance(verb_cancel, bytes):
raise NotImplementedError
elif isinstance(verb, str):
if isinstance(verb, str):
verb = verb.upper()
if isinstance(verb_cancel, str):
verb_cancel = verb_cancel.upper()
if description is not None and description_param is not None:
if description_param_font != ui.BOLD:
log.error(__name__, "confirm_action description_param_font not implemented")
description = description.format(description_param)
if hold:
log.error(__name__, "confirm_action hold not implemented")
if verb_cancel:
log.error(__name__, "confirm_action verb_cancel not implemented")
result = await interact(
ctx,
_RustLayout(
@ -142,6 +139,7 @@ async def confirm_action(
action=action,
description=description,
verb=verb,
verb_cancel=verb_cancel,
hold=hold,
reverse=reverse,
)
@ -172,21 +170,51 @@ async def confirm_backup(ctx: wire.GenericContext) -> bool:
async def confirm_path_warning(
ctx: wire.GenericContext, path: str, path_type: str = "Path"
) -> None:
raise NotImplementedError
result = await interact(
ctx,
_RustLayout(
trezorui2.show_warning(
title="Unknown path",
description=path,
)
),
"path_warning",
ButtonRequestType.UnknownDerivationPath,
)
if result is not trezorui2.CONFIRMED:
raise wire.ActionCancelled
def _show_xpub(xpub: str, title: str, cancel: str) -> ui.Layout:
content = _RustLayout(
trezorui2.confirm_blob(
title=title,
data=xpub,
verb_cancel=cancel,
)
)
return content
async def show_xpub(
ctx: wire.GenericContext, xpub: str, title: str, cancel: str
) -> None:
raise NotImplementedError
result = await interact(
ctx,
_show_xpub(xpub, title, cancel),
"show_xpub",
ButtonRequestType.PublicKey,
)
if result is not trezorui2.CONFIRMED:
raise wire.ActionCancelled
async def show_address(
ctx: wire.GenericContext,
address: str,
*,
case_sensitive: bool = True,
address_qr: str | None = None,
case_sensitive: bool = True,
title: str = "Confirm address",
network: str | None = None,
multisig_index: int | None = None,
@ -194,7 +222,54 @@ async def show_address(
address_extra: str | None = None,
title_qr: str | None = None,
) -> None:
raise NotImplementedError
is_multisig = len(xpubs) > 0
while True:
result = await interact(
ctx,
_RustLayout(
trezorui2.confirm_blob(
title=title.upper(),
data=address,
description=network or "",
extra=address_extra or "",
verb_cancel="QR",
)
),
"show_address",
ButtonRequestType.Address,
)
if result is trezorui2.CONFIRMED:
break
result = await interact(
ctx,
_RustLayout(
trezorui2.show_qr(
address=address if address_qr is None else address_qr,
case_sensitive=case_sensitive,
title=title.upper() if title_qr is None else title_qr.upper(),
verb_cancel="XPUBs" if is_multisig else "ADDRESS",
)
),
"show_qr",
ButtonRequestType.Address,
)
if result is trezorui2.CONFIRMED:
break
if is_multisig:
for i, xpub in enumerate(xpubs):
cancel = "NEXT" if i < len(xpubs) - 1 else "ADDRESS"
title_xpub = f"XPUB #{i + 1}"
title_xpub += " (yours)" if i == multisig_index else " (cosigner)"
result = await interact(
ctx,
_show_xpub(xpub, title=title_xpub, cancel=cancel),
"show_xpub",
ButtonRequestType.PublicKey,
)
if result is trezorui2.CONFIRMED:
return
def show_pubkey(
@ -277,25 +352,27 @@ def show_warning(
)
def show_success(
async def show_success(
ctx: wire.GenericContext,
br_type: str,
content: str,
subheader: str | None = None,
button: str = "Continue",
) -> Awaitable[None]:
return _show_modal(
) -> None:
result = await interact(
ctx,
br_type=br_type,
br_code=ButtonRequestType.Success,
header="Success",
subheader=subheader,
content=content,
button_confirm=button,
button_cancel=None,
icon=ui.ICON_CONFIRM,
icon_color=ui.GREEN,
_RustLayout(
trezorui2.show_success(
title=content,
description=subheader or "",
button=button.upper(),
)
),
br_type,
ButtonRequestType.Success,
)
if result is not trezorui2.CONFIRMED:
raise wire.ActionCancelled
async def confirm_output(
@ -303,7 +380,7 @@ async def confirm_output(
address: str,
amount: str,
font_amount: int = ui.NORMAL, # TODO cleanup @ redesign
title: str = "Confirm sending",
title: str = "SENDING",
subtitle: str | None = None, # TODO cleanup @ redesign
color_to: int = ui.FG, # TODO cleanup @ redesign
to_str: str = " to\n", # TODO cleanup @ redesign
@ -313,7 +390,39 @@ async def confirm_output(
br_code: ButtonRequestType = ButtonRequestType.ConfirmOutput,
icon: str = ui.ICON_SEND,
) -> None:
raise NotImplementedError
title = title.upper()
if title.startswith("CONFIRM "):
title = title[len("CONFIRM ") :]
result = await interact(
ctx,
_RustLayout(
trezorui2.confirm_output(
title=title,
description="To:",
value=address,
)
),
"confirm_output",
br_code,
)
if result is not trezorui2.CONFIRMED:
raise wire.ActionCancelled
result = await interact(
ctx,
_RustLayout(
trezorui2.confirm_output(
title=title,
description="Amount:",
value=amount,
)
),
"confirm_output",
br_code,
)
if result is not trezorui2.CONFIRMED:
raise wire.ActionCancelled
async def confirm_payment_request(
@ -322,7 +431,25 @@ async def confirm_payment_request(
amount: str,
memos: list[str],
) -> Any:
raise NotImplementedError
from ...components.common import confirm
result = await interact(
ctx,
_RustLayout(
trezorui2.confirm_payment_request(
description=f"{amount} to\n{recipient_name}",
memos=memos,
)
),
"confirm_payment_request",
ButtonRequestType.ConfirmOutput,
)
if result is trezorui2.CONFIRMED:
return confirm.CONFIRMED
elif result is trezorui2.INFO:
return confirm.INFO
else:
raise wire.ActionCancelled
async def should_show_more(
@ -350,7 +477,25 @@ async def confirm_blob(
icon_color: int = ui.GREEN, # TODO cleanup @ redesign
ask_pagination: bool = False,
) -> None:
raise NotImplementedError
if isinstance(data, bytes):
data = hexlify(data).decode()
result = await interact(
ctx,
_RustLayout(
trezorui2.confirm_blob(
title=title.upper(),
description=description or "",
data=data,
ask_pagination=ask_pagination,
hold=hold,
)
),
br_type,
br_code,
)
if result is not trezorui2.CONFIRMED:
raise wire.ActionCancelled
def confirm_address(
@ -410,20 +555,60 @@ async def confirm_total(
total_amount: str,
fee_amount: str,
fee_rate_amount: str | None = None,
title: str = "Confirm transaction",
title: str = "SENDING",
total_label: str = "Total amount:\n",
fee_label: str = "\nincluding fee:\n",
icon_color: int = ui.GREEN,
br_type: str = "confirm_total",
br_code: ButtonRequestType = ButtonRequestType.SignTx,
) -> None:
raise NotImplementedError
result = await interact(
ctx,
_RustLayout(
trezorui2.confirm_output(
title=title.upper(),
description="Fee:",
value=fee_amount,
)
),
"confirm_total",
br_code,
)
if result is not trezorui2.CONFIRMED:
raise wire.ActionCancelled
result = await interact(
ctx,
_RustLayout(
trezorui2.confirm_total(
title=title.upper(),
description="Total amount:",
value=total_amount,
)
),
"confirm_total",
br_code,
)
if result is not trezorui2.CONFIRMED:
raise wire.ActionCancelled
async def confirm_joint_total(
ctx: wire.GenericContext, spending_amount: str, total_amount: str
) -> None:
raise NotImplementedError
result = await interact(
ctx,
_RustLayout(
trezorui2.confirm_joint_total(
spending_amount=spending_amount,
total_amount=total_amount,
)
),
"confirm_joint_total",
ButtonRequestType.SignTx,
)
if result is not trezorui2.CONFIRMED:
raise wire.ActionCancelled
async def confirm_metadata(
@ -440,13 +625,59 @@ async def confirm_metadata(
icon_color: int = ui.GREEN, # TODO cleanup @ redesign
larger_vspace: bool = False, # TODO cleanup @ redesign
) -> None:
raise NotImplementedError
if param:
content = content.format(param)
if br_type == "fee_over_threshold":
layout = trezorui2.show_warning(
title="Unusually high fee",
description=param or "",
)
elif br_type == "change_count_over_threshold":
layout = trezorui2.show_warning(
title="A lot of change-outputs",
description=f"{param} outputs" if param is not None else "",
)
else:
if param is not None:
content = content.format(param)
# TODO: "unverified external inputs"
layout = trezorui2.confirm_action(
title=title.upper(),
verb="NEXT",
description=content,
hold=hold,
)
result = await interact(
ctx,
_RustLayout(layout),
br_type,
br_code,
)
if result is not trezorui2.CONFIRMED:
raise wire.ActionCancelled
async def confirm_replacement(
ctx: wire.GenericContext, description: str, txid: str
) -> None:
raise NotImplementedError
result = await interact(
ctx,
_RustLayout(
trezorui2.confirm_blob(
title=description.upper(),
description="Confirm transaction ID:",
data=txid,
)
),
"confirm_replacement",
ButtonRequestType.SignTx,
)
if result is not trezorui2.CONFIRMED:
raise wire.ActionCancelled
async def confirm_modify_output(
@ -456,7 +687,21 @@ async def confirm_modify_output(
amount_change: str,
amount_new: str,
) -> None:
raise NotImplementedError
result = await interact(
ctx,
_RustLayout(
trezorui2.confirm_modify_output(
address=address,
sign=sign,
amount_change=amount_change,
amount_new=amount_new,
)
),
"modify_output",
ButtonRequestType.ConfirmOutput,
)
if result is not trezorui2.CONFIRMED:
raise wire.ActionCancelled
async def confirm_modify_fee(
@ -465,13 +710,39 @@ async def confirm_modify_fee(
user_fee_change: str,
total_fee_new: str,
) -> None:
raise NotImplementedError
result = await interact(
ctx,
_RustLayout(
trezorui2.confirm_modify_fee(
sign=sign,
user_fee_change=user_fee_change,
total_fee_new=total_fee_new,
)
),
"modify_fee",
ButtonRequestType.SignTx,
)
if result is not trezorui2.CONFIRMED:
raise wire.ActionCancelled
async def confirm_coinjoin(
ctx: wire.GenericContext, coin_name: str, max_rounds: int, max_fee_per_vbyte: str
) -> None:
raise NotImplementedError
result = await interact(
ctx,
_RustLayout(
trezorui2.confirm_coinjoin(
coin_name=coin_name,
max_rounds=str(max_rounds),
max_feerate=f"{max_fee_per_vbyte} sats/vbyte",
)
),
"coinjoin_final",
ButtonRequestType.Other,
)
if result is not trezorui2.CONFIRMED:
raise wire.ActionCancelled
# TODO cleanup @ redesign
@ -484,7 +755,42 @@ async def confirm_sign_identity(
async def confirm_signverify(
ctx: wire.GenericContext, coin: str, message: str, address: str, verify: bool
) -> None:
raise NotImplementedError
if verify:
title = f"VERIFY {coin} MESSAGE"
br_type = "verify_message"
else:
title = f"SIGN {coin} MESSAGE"
br_type = "sign_message"
result = await interact(
ctx,
_RustLayout(
trezorui2.confirm_blob(
title=title,
description="Confirm address:",
data=address,
)
),
br_type,
ButtonRequestType.Other,
)
if result is not trezorui2.CONFIRMED:
raise wire.ActionCancelled
result = await interact(
ctx,
_RustLayout(
trezorui2.confirm_blob(
title=title,
description="Confirm message:",
data=message,
)
),
br_type,
ButtonRequestType.Other,
)
if result is not trezorui2.CONFIRMED:
raise wire.ActionCancelled
async def show_popup(

File diff suppressed because it is too large Load Diff