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:
parent
dd9a7d30e5
commit
c9ca7cd544
BIN
core/assets/error.png
Normal file
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
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
BIN
core/assets/warn.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
@ -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")
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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
Normal file
75
core/embed/rust/src/trezorhal/qr.rs
Normal 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(())
|
||||
}
|
||||
}
|
@ -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
Normal file
42
core/embed/rust/src/ui/component/image.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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>;
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -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::{
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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}",
|
||||
|
BIN
core/embed/rust/src/ui/model_tt/res/error.toif
Normal file
BIN
core/embed/rust/src/ui/model_tt/res/error.toif
Normal file
Binary file not shown.
BIN
core/embed/rust/src/ui/model_tt/res/success.toif
Normal file
BIN
core/embed/rust/src/ui/model_tt/res/success.toif
Normal file
Binary file not shown.
BIN
core/embed/rust/src/ui/model_tt/res/warn.toif
Normal file
BIN
core/embed/rust/src/ui/model_tt/res/warn.toif
Normal file
Binary file not shown.
@ -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 {
|
||||
|
@ -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:
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user